Merge branch 'master' into dummy_pr_ci_verfication

This commit is contained in:
Darshan 2026-05-21 17:58:03 +05:30 committed by GitHub
commit cb84ec1074
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
121 changed files with 4716 additions and 4305 deletions

View File

@ -18,7 +18,7 @@ For detailed info on the release process refer to https://github.com/logos-messa
All items below are to be completed by the owner of the given release.
- [ ] Create release branch with major and minor only ( e.g. release/v0.X ) if it doesn't exist.
- [ ] Update the `version` field in `waku.nimble` to match the release version (e.g. `version = "0.X.0"`).
- [ ] Update the `version` field in `waku.nimble` to match the release version (e.g. `version = "0.X.0"`) **and merge it before assigning any tag** - the `release-assets` workflow gates artifact build/upload.
- [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-rc.0`, `v0.X.0-rc.1`, ... `v0.X.0-rc.N`).
- [ ] Generate and edit release notes in CHANGELOG.md.

View File

@ -11,7 +11,35 @@ env:
NPROC: 2
jobs:
# Release gate: the pushed tag MUST exactly match waku.nimble's version,
# so every published artifact reports the correct getNodeInfo Version.
# CI cannot reject/remove a tag, so we gate artifact build & upload on
# this instead: a mismatched tag yields no released artifacts.
verify-version:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Assert pushed tag equals waku.nimble version
if: startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
NIMBLE_VERSION=$(grep -m1 '^version = ' waku.nimble | sed -E 's/version = "([^"]+)"/\1/')
# Strip leading v and any prerelease suffix (e.g. v0.38.0-rc.1 ->
# 0.38.0) so release-candidate tags build against the same
# waku.nimble version as the final tag.
TAG_VERSION="${GITHUB_REF_NAME#v}"
BASE_VERSION="${TAG_VERSION%%-*}"
echo "tag: ${GITHUB_REF_NAME} (base ${BASE_VERSION})"
echo "waku.nimble version: ${NIMBLE_VERSION}"
if [ "${BASE_VERSION}" != "${NIMBLE_VERSION}" ]; then
echo "::error::Tag ${GITHUB_REF_NAME} (base ${BASE_VERSION}) does not match"
echo "::error::waku.nimble version (${NIMBLE_VERSION}). Bump waku.nimble before tagging."
exit 1
fi
echo "OK: tag base matches waku.nimble."
build-and-upload:
needs: verify-version
strategy:
matrix:
os: [ubuntu-22.04, macos-15]

49
.github/workflows/version-check.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: version check
permissions:
contents: read
on:
pull_request:
branches: [master]
jobs:
# PR check: waku.nimble version must be >= the nearest tag reachable from
# this branch (`git describe --tags --abbrev=0`, i.e. ancestor-aware).
# Because we check out the PR HEAD (not the simulated merge ref), a branch
# that predates a release tag does not see that tag in its history, so a
# newly pushed tag does NOT break in-flight PRs. Once the branch merges/
# rebases past the tag, the bump is then enforced. This keeps waku.nimble
# fixed as early as possible, independent of whether a release is cut.
# The exact tag==nimble guarantee at release time lives in
# release-assets.yml, which gates artifact publishing on it.
nimble-not-behind-tag:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Compare waku.nimble version with nearest ancestor tag
run: |
set -euo pipefail
NIMBLE_VERSION=$(grep -m1 '^version = ' waku.nimble | sed -E 's/version = "([^"]+)"/\1/')
# Nearest tag reachable from HEAD; --abbrev=0 drops the -<n>-g<sha>
# suffix so we get the bare tag (e.g. v0.38.0).
BASE_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
BASE_TAG=${BASE_TAG#v}
# Compare on the base version, ignoring any -rc.N prerelease suffix.
BASE_TAG=${BASE_TAG%%-*}
echo "waku.nimble version: ${NIMBLE_VERSION}"
echo "ancestor git tag: ${BASE_TAG:-<none>}"
if [ -z "${BASE_TAG}" ]; then
echo "No ancestor release tag; skipping."
exit 0
fi
# lowest of the two by version sort must be the tag => nimble >= tag
LOWEST=$(printf '%s\n%s\n' "${NIMBLE_VERSION}" "${BASE_TAG}" | sort -V | head -1)
if [ "${LOWEST}" != "${BASE_TAG}" ] && [ "${NIMBLE_VERSION}" != "${BASE_TAG}" ]; then
echo "::error::waku.nimble version (${NIMBLE_VERSION}) is behind its"
echo "::error::ancestor git tag (v${BASE_TAG}). Bump 'version' in waku.nimble."
exit 1
fi
echo "OK: waku.nimble is not behind its ancestor tag."

1
.gitignore vendored
View File

@ -85,3 +85,4 @@ nimble.paths
nimbledeps
**/anvil_state/state-deployed-contracts-mint-and-approved.json
.gitnexus

View File

@ -506,4 +506,46 @@ Language: Nim 2.x | License: MIT or Apache 2.0
Note: For specific version requirements, check `waku.nimble`.
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **logos-delivery** (2076 symbols, 2564 relationships, 12 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
## Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
## Resources
| Resource | Use for |
|----------|---------|
| `gitnexus://repo/logos-delivery/context` | Codebase overview, check index freshness |
| `gitnexus://repo/logos-delivery/clusters` | All functional areas |
| `gitnexus://repo/logos-delivery/processes` | All execution flows |
| `gitnexus://repo/logos-delivery/process/{name}` | Step-by-step execution trace |
## CLI
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
<!-- gitnexus:end -->

View File

@ -1,3 +1,11 @@
## v0.38.1 (2026-05-07)
### Changes
- Evict peer instead of abrupt disconnect and avoid sending unnecessary store requests ([#3857](https://github.com/logos-messaging/logos-delivery/pull/3857)) ([75dbeb1b](https://github.com/logos-messaging/logos-delivery/commit/75dbeb1be785df5e61c9ab0bcf8393349b9d0f5e) and [7e59b2c2](https://github.com/logos-messaging/logos-delivery/commit/7e59b2c2))
- RecvService now delivers store-recovered messages via MessageReceivedEvent and includes check for missed hashes before processing ([#3805](https://github.com/logos-messaging/logos-delivery/pull/3805)) ([494ea946](https://github.com/logos-messaging/logos-delivery/commit/494ea946))
## v0.38.0 (2026-03-16)
### Notes

View File

@ -176,7 +176,7 @@ deps: | nimble
.PHONY: librln
LIBRLN_BUILDDIR := $(CURDIR)/vendor/zerokit
LIBRLN_VERSION := v0.9.0
LIBRLN_VERSION := v2.0.2
ifeq ($(detected_OS),Windows)
LIBRLN_FILE ?= rln.lib

View File

@ -13,7 +13,8 @@ import
chronos,
eth/keys,
bearssl,
stew/[byteutils, results],
stew/[byteutils],
results,
metrics,
metrics/chronos_httpserver
import

View File

@ -140,7 +140,8 @@ type
metricsServerAddress* {.
desc: "Listening address of the metrics server.",
defaultValue: parseIpAddress("127.0.0.1"),
defaultValue:
IpAddress(family: IpAddressFamily.IPv4, address_v4: [127'u8, 0, 0, 1]),
name: "metrics-server-address"
.}: IpAddress
@ -173,7 +174,10 @@ type
dnsAddrsNameServers* {.
desc:
"DNS name server IPs to query for DNS multiaddrs resolution. Argument may be repeated.",
defaultValue: @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")],
defaultValue: @[
IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 1, 1, 1]),
IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 0, 0, 1]),
],
name: "dns-addrs-name-server"
.}: seq[IpAddress]
@ -348,4 +352,4 @@ proc parseCmdArg*(T: type EthRpcUrl, s: string): T =
func defaultListenAddress*(conf: Chat2Conf): IpAddress =
# TODO: How should we select between IPv4 and IPv6
# Maybe there should be a config option for this.
(static parseIpAddress("0.0.0.0"))
(static IpAddress(family: IpAddressFamily.IPv4, address_v4: [0'u8, 0, 0, 0]))

View File

@ -117,7 +117,7 @@ if defined(android):
switch("passL", "--sysroot=" & sysRoot)
switch("cincludes", sysRoot & "/usr/include/")
# begin Nimble config (version 2)
--noNimblePath
when withDir(thisDir(), system.fileExists("nimble.paths")):
--noNimblePath
include "nimble.paths"
# end Nimble config

View File

@ -33,9 +33,9 @@ proc periodicSender(w: Waku): Future[void] {.async.} =
return
defer:
MessageSentEvent.dropListener(sentListener)
MessageErrorEvent.dropListener(errorListener)
MessagePropagatedEvent.dropListener(propagatedListener)
await MessageSentEvent.dropListener(sentListener)
await MessageErrorEvent.dropListener(errorListener)
await MessagePropagatedEvent.dropListener(propagatedListener)
## Periodically sends a Waku message every 30 seconds
var counter = 0

6
flake.lock generated
View File

@ -72,17 +72,15 @@
"rust-overlay": "rust-overlay_2"
},
"locked": {
"lastModified": 1771279884,
"narHash": "sha256-tzkQPwSl4vPTUo1ixHh6NCENjsBDroMKTjifg2q8QX8=",
"owner": "vacp2p",
"repo": "zerokit",
"rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477",
"rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63",
"type": "github"
},
"original": {
"owner": "vacp2p",
"repo": "zerokit",
"rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477",
"rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63",
"type": "github"
}
}

View File

@ -21,7 +21,10 @@
# External flake input: Zerokit pinned to a specific commit.
# Update the rev here when a new zerokit version is needed.
zerokit = {
url = "github:vacp2p/zerokit/53b18098e6d5d046e3eb1ac338a8f4f651432477";
# Pinned to v2.0.2 (5e64cb8822bee65eed6cf459f95ae72b80c6ba63) to match
# the vendor/zerokit submodule. Keep these two in sync: the nix build
# links librln from this input, the Makefile build from the submodule.
url = "github:vacp2p/zerokit/5e64cb8822bee65eed6cf459f95ae72b80c6ba63";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@ -36,6 +39,20 @@
forAllSystems = nixpkgs.lib.genAttrs systems;
lib = nixpkgs.lib;
# Single source of truth for the semver: the `version` field of
# waku.nimble. Kept in sync with git tags by the version-check CI.
nimbleVersion =
let line = lib.findFirst (l: lib.hasPrefix "version = " l)
"version = \"unknown\""
(lib.splitString "\n" (builtins.readFile ./waku.nimble));
in lib.removeSuffix "\"" (lib.removePrefix "version = \"" line);
# A flake sandbox has no .git, so `git describe` is impossible; the
# commit comes from the flake metadata instead.
shortRev = self.shortRev or self.dirtyShortRev or "dirty";
nimbleOverlay = final: prev: {
nimble = prev.nimble.overrideAttrs (_: {
version = "0.22.3";
@ -56,10 +73,42 @@
packages = forAllSystems (system:
let
pkgs = pkgsFor system;
# zerokit's nix/default.nix hardcodes a cargoHash that is stale for
# our pinned nixpkgs on a cold runner (the status.im substituter is
# untrusted here, so the cargo-vendor FOD is recomputed). v2.0.2 did
# NOT fix this for consumers — its committed hash is the old v2.0.1
# value while v2.0.2's Cargo.lock changed. Rebuild librln here from
# the pinned zerokit source with the correct cargoHash. Keep the
# version + cargoHash in sync with the zerokit input rev.
rustToolchain = pkgs.rust-bin.stable.latest.default;
zerokitRln = pkgs.rustPlatform.buildRustPackage {
pname = "zerokit";
version = "2.0.2";
src = zerokit;
cargo = rustToolchain;
rustc = rustToolchain;
cargoHash = "sha256-PNwEdZLgGQPqQDrEK2hsQtSybVfBbD6xn4K47fPFJUU=";
nativeBuildInputs = [ pkgs.rust-cbindgen ];
doCheck = false;
buildPhase = ''
export CARGO_HOME=$TMPDIR/cargo
cargo build --lib --release --manifest-path rln/Cargo.toml
'';
installPhase = ''
set -eu
mkdir -p $out/lib $out/include
find target -type f -name 'librln.*' -not -path '*/deps/*' \
-exec cp -v '{}' "$out/lib/" \;
cbindgen ./rln -l c > "$out/include/rln.h"
'';
};
liblogosdelivery = pkgs.callPackage ./nix/default.nix {
inherit pkgs;
src = ./.;
zerokitRln = zerokit.packages.${system}.rln;
inherit zerokitRln;
gitVersion = "v${nimbleVersion}-g${builtins.substring 0 6 shortRev}";
};
in {
inherit liblogosdelivery;

View File

@ -184,11 +184,11 @@ proc logosdelivery_stop_node(
requireInitializedNode(ctx, "STOP_NODE"):
return err(errMsg)
MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx)
MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx)
MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx)
MessageReceivedEvent.dropAllListeners(ctx.myLib[].brokerCtx)
EventConnectionStatusChange.dropAllListeners(ctx.myLib[].brokerCtx)
await MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx)
await MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx)
await MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx)
await MessageReceivedEvent.dropAllListeners(ctx.myLib[].brokerCtx)
await EventConnectionStatusChange.dropAllListeners(ctx.myLib[].brokerCtx)
(await ctx.myLib[].stop()).isOkOr:
let errMsg = $error

View File

@ -263,6 +263,21 @@
"sha1": "8bc8c30b107fdba73b677e5f257c6c42ae1cdc8e"
}
},
"cbor_serialization": {
"version": "0.3.0",
"vcsRevision": "1664160e04d153573373afddc552b9cbf6fbe4dc",
"url": "https://github.com/vacp2p/nim-cbor-serialization",
"downloadMethod": "git",
"dependencies": [
"nim",
"serialization",
"stew",
"results"
],
"checksums": {
"sha1": "ab126eae09a6e39c72972a6a0b83cb06a2ffe8f0"
}
},
"json_serialization": {
"version": "0.4.4",
"vcsRevision": "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44",
@ -312,6 +327,23 @@
"sha1": "8df97c45683abe2337bdff43b844c4fbcc124ca2"
}
},
"brokers": {
"version": "#v2.0.1",
"vcsRevision": "2093ca4d50e581adda73fee7fd16231f990f4cbe",
"url": "https://github.com/NagyZoltanPeter/nim-brokers.git",
"downloadMethod": "git",
"dependencies": [
"nim",
"chronos",
"results",
"chronicles",
"testutils",
"cbor_serialization"
],
"checksums": {
"sha1": "cc74c987af94537e9d44d1b0143aa417299040c5"
}
},
"stint": {
"version": "0.8.2",
"vcsRevision": "470b7892561b5179ab20bd389a69217d6213fe58",
@ -587,6 +619,26 @@
"sha1": "09e1b2fdad55b973724d61227971afc0df0b7a81"
}
},
"sds": {
"version": "#2e9a7683f0e180bf112135fae3a3803eed8490d4",
"vcsRevision": "2e9a7683f0e180bf112135fae3a3803eed8490d4",
"url": "https://github.com/logos-messaging/nim-sds.git",
"downloadMethod": "git",
"dependencies": [
"nim",
"chronos",
"libp2p",
"chronicles",
"stew",
"stint",
"metrics",
"results",
"taskpools"
],
"checksums": {
"sha1": "d13f1bf8d1b90b27e9edfc063b043831242cda19"
}
},
"ffi": {
"version": "0.1.3",
"vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b",

View File

@ -1,6 +1,7 @@
{ pkgs
, src
, zerokitRln
, gitVersion ? "n/a"
, enablePostgres ? true
, enableNimDebugDlOpen ? true
, chroniclesLogLevel ? null
@ -10,7 +11,8 @@ let
deps = import ./deps.nix { inherit pkgs; };
nimDefineArgs = pkgs.lib.concatStringsSep " \\\n " (
[ "--define:disable_libbacktrace" ]
[ "--define:disable_libbacktrace"
"--define:git_version=${gitVersion}" ]
++ pkgs.lib.optional enablePostgres "--define:postgres"
++ pkgs.lib.optional enableNimDebugDlOpen "--define:nimDebugDlOpen"
++ pkgs.lib.optional (chroniclesLogLevel != null)

View File

@ -129,6 +129,13 @@
fetchSubmodules = true;
};
cbor_serialization = pkgs.fetchgit {
url = "https://github.com/vacp2p/nim-cbor-serialization";
rev = "1664160e04d153573373afddc552b9cbf6fbe4dc";
sha256 = "0c1rj4fk0fcqvsf0yqhxvm8h10aww75gi4yfsjhlczh88ypywii2";
fetchSubmodules = true;
};
json_serialization = pkgs.fetchgit {
url = "https://github.com/status-im/nim-json-serialization";
rev = "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44";
@ -150,6 +157,13 @@
fetchSubmodules = true;
};
brokers = pkgs.fetchgit {
url = "https://github.com/NagyZoltanPeter/nim-brokers.git";
rev = "2093ca4d50e581adda73fee7fd16231f990f4cbe";
sha256 = "0a4ix2q6riqfrd0hfnajisy159qdmk5imwzymppj23rwc8n7d2dx";
fetchSubmodules = true;
};
stint = pkgs.fetchgit {
url = "https://github.com/status-im/nim-stint";
rev = "470b7892561b5179ab20bd389a69217d6213fe58";
@ -262,6 +276,13 @@
fetchSubmodules = true;
};
sds = pkgs.fetchgit {
url = "https://github.com/logos-messaging/nim-sds.git";
rev = "2e9a7683f0e180bf112135fae3a3803eed8490d4";
sha256 = "1dbpvp3zhvdlfxdyggz5waga1vg3b6ndd3acfzhnx8k1wdr01c6f";
fetchSubmodules = true;
};
ffi = pkgs.fetchgit {
url = "https://github.com/logos-messaging/nim-ffi";
rev = "06111de155253b34e47ed2aaed1d61d08d62cc1b";

View File

@ -33,8 +33,16 @@ if [[ "v${submodule_version}" != "${rln_version}" ]]; then
exit 1
fi
# Build rln from source
cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml"
# Build rln from source.
# `stateless` feature: logos-delivery does not maintain a local Merkle tree
# (post-PR #3312); the contract is the source of truth and the path is fetched
# via getMerkleProof(index). The stateless build compiles out tree code.
#
# --no-default-features is required because zerokit's default features include
# `pmtree-ft` (a Merkle tree backend); `stateless` and any Merkle-tree feature
# are mutually exclusive (rln/src/lib.rs:32 compile_error).
cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" \
--no-default-features --features stateless
cp "${build_dir}/target/release/librln.a" "${output_filename}"
echo "Successfully built ${output_filename}"

View File

@ -85,3 +85,6 @@ import ./api/test_all
# Waku tools tests
import ./tools/test_all
# Persistency library tests
import ./persistency/test_all

View File

@ -2,11 +2,12 @@
import std/[options, sequtils, times]
import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo]
import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync]
import
waku,
waku/[waku_node, waku_core, waku_relay/protocol, common/broker/broker_context],
waku/[waku_node, waku_core, waku_relay/protocol],
waku/node/health_monitor/[topic_health, health_status, protocol_health, health_report],
waku/requests/health_requests,
waku/requests/node_requests,
@ -43,7 +44,7 @@ proc waitForConnectionStatus(
if not await future.withTimeout(TestTimeout):
raiseAssert "Timeout waiting for status: " & $expected
finally:
EventConnectionStatusChange.dropListener(brokerCtx, handle)
await EventConnectionStatusChange.dropListener(brokerCtx, handle)
proc waitForShardHealthy(
brokerCtx: BrokerContext
@ -67,7 +68,7 @@ proc waitForShardHealthy(
else:
raiseAssert "Timeout waiting for shard health event"
finally:
EventShardTopicHealthChange.dropListener(brokerCtx, handle)
await EventShardTopicHealthChange.dropListener(brokerCtx, handle)
suite "LM API health checking":
var

View File

@ -3,6 +3,7 @@
import std/[options, sequtils, net, sets]
import chronos, testutils/unittests, stew/byteutils
import libp2p/[peerid, peerinfo, crypto/crypto]
import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync]
import ../waku_archive/archive_utils
@ -11,7 +12,6 @@ import
waku/[
waku_node,
waku_core,
common/broker/broker_context,
events/message_events,
waku_relay/protocol,
waku_archive,
@ -52,8 +52,8 @@ proc newReceiveEventListenerManager(
return manager
proc teardown(manager: ReceiveEventListenerManager) =
MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener)
proc teardown(manager: ReceiveEventListenerManager) {.async.} =
await MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener)
proc waitForEvents(
manager: ReceiveEventListenerManager, timeout: Duration
@ -138,7 +138,20 @@ suite "Messaging API, Receive Service (store recovery)":
break
await sleepAsync(100.milliseconds)
# publish before subscriber exists, gets archived
# create the subscriber before publishing.
# RecvService captures startTimeToCheck at construction time; the
# message's timestamp must land after that point to fall inside
# checkStore's time window.
var subscriber: Waku
lockNewGlobalBrokerContext:
subscriber = (await createNode(createApiNodeConf(numShards))).expect(
"Failed to create subscriber"
)
(await startWaku(addr subscriber)).expect("Failed to start subscriber")
# publish after the subscriber exists but before it connects to the
# store; the message reaches the archive but the subscriber doesn't
# see it via live relay.
let missedPayload = "This message was missed".toBytes()
let missedMsg = WakuMessage(
payload: missedPayload, contentTopic: testTopic, version: 0, timestamp: now()
@ -159,15 +172,8 @@ suite "Messaging API, Receive Service (store recovery)":
await sleepAsync(100.milliseconds)
raiseAssert "Message was not archived in time"
# create subscriber
var subscriber: Waku
lockNewGlobalBrokerContext:
subscriber = (await createNode(createApiNodeConf(numShards))).expect(
"Failed to create subscriber"
)
(await startWaku(addr subscriber)).expect("Failed to start subscriber")
# connect subscriber to store (not publisher, so msg won't come via relay to it)
# connect subscriber to store after the message is already archived so
# gossipsub doesn't replay it via the live path
await subscriber.node.connectToNodes(@[storeNodePeerInfo])
# subscribe to content topic
@ -176,7 +182,7 @@ suite "Messaging API, Receive Service (store recovery)":
# listen before triggering store check
let eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
# trigger store check, should recover and deliver via MessageReceivedEvent
await subscriber.deliveryService.recvService.checkStore()

View File

@ -2,10 +2,10 @@
import std/strutils
import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo]
import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync]
import ../waku_archive/archive_utils
import
waku, waku/[waku_node, waku_core, waku_relay/protocol, common/broker/broker_context]
import waku, waku/[waku_node, waku_core, waku_relay/protocol]
import waku/factory/waku_conf
import tools/confutils/cli_args
@ -77,10 +77,12 @@ proc newSendEventListenerManager(brokerCtx: BrokerContext): SendEventListenerMan
return manager
proc teardown(manager: SendEventListenerManager) =
MessageSentEvent.dropListener(manager.brokerCtx, manager.sentListener)
MessageErrorEvent.dropListener(manager.brokerCtx, manager.errorListener)
MessagePropagatedEvent.dropListener(manager.brokerCtx, manager.propagatedListener)
proc teardown(manager: SendEventListenerManager) {.async.} =
await MessageSentEvent.dropListener(manager.brokerCtx, manager.sentListener)
await MessageErrorEvent.dropListener(manager.brokerCtx, manager.errorListener)
await MessagePropagatedEvent.dropListener(
manager.brokerCtx, manager.propagatedListener
)
proc waitForEvents(
manager: SendEventListenerManager, timeout: Duration
@ -270,7 +272,7 @@ suite "Waku API - Send":
let eventManager = newSendEventListenerManager(node.brokerCtx)
defer:
eventManager.teardown()
await eventManager.teardown()
let envelope = MessageEnvelope.init(
ContentTopic("/waku/2/default-content/proto"), "test payload"
@ -302,7 +304,7 @@ suite "Waku API - Send":
let eventManager = newSendEventListenerManager(node.brokerCtx)
defer:
eventManager.teardown()
await eventManager.teardown()
let envelope = MessageEnvelope.init(
ContentTopic("/waku/2/default-content/proto"), "test payload"
@ -332,7 +334,7 @@ suite "Waku API - Send":
let eventManager = newSendEventListenerManager(node.brokerCtx)
defer:
eventManager.teardown()
await eventManager.teardown()
let envelope = MessageEnvelope.init(
ContentTopic("/waku/2/default-content/proto"), "test payload"
@ -362,7 +364,7 @@ suite "Waku API - Send":
let eventManager = newSendEventListenerManager(node.brokerCtx)
defer:
eventManager.teardown()
await eventManager.teardown()
let envelope = MessageEnvelope.init(
ContentTopic("/waku/2/default-content/proto"), "test payload"
@ -416,7 +418,7 @@ suite "Waku API - Send":
let eventManager = newSendEventListenerManager(node.brokerCtx)
defer:
eventManager.teardown()
await eventManager.teardown()
let envelope = MessageEnvelope.init(
ContentTopic("/waku/2/default-content/proto"), "test payload"

View File

@ -3,6 +3,7 @@
import std/[strutils, sequtils, net, options, sets, tables]
import chronos, testutils/unittests, stew/byteutils
import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto]
import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync]
import
@ -10,7 +11,6 @@ import
waku/[
waku_node,
waku_core,
common/broker/broker_context,
events/message_events,
waku_relay/protocol,
node/kernel_api/filter,
@ -51,8 +51,8 @@ proc newReceiveEventListenerManager(
return manager
proc teardown(manager: ReceiveEventListenerManager) =
MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener)
proc teardown(manager: ReceiveEventListenerManager) {.async.} =
await MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener)
proc waitForEvents(
manager: ReceiveEventListenerManager, timeout: Duration
@ -208,7 +208,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMesh(testTopic, "Hello, world!".toBytes())).expect(
"Publish failed"
@ -229,7 +229,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect(
"Publish failed"
@ -250,7 +250,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect(
"Publish failed"
@ -271,7 +271,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
net.subscriber.unsubscribe(topicA).expect("failed to unsub A")
@ -298,7 +298,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMesh(glitchTopic, "Ghost Msg".toBytes())).expect(
"Publish failed"
@ -322,7 +322,7 @@ suite "Messaging API, SubscriptionManager":
(await net.publishToMesh(testTopic, "Msg 1".toBytes())).expect("Pub 1 failed")
require await eventManager.waitForEvents(TestTimeout)
eventManager.teardown()
await eventManager.teardown()
# Unsubscribe and verify teardown
net.subscriber.unsubscribe(testTopic).expect("Unsub failed")
@ -332,7 +332,7 @@ suite "Messaging API, SubscriptionManager":
(await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed")
check not await eventManager.waitForEvents(NegativeTestTimeout)
eventManager.teardown()
await eventManager.teardown()
# Resubscribe
(await net.subscriber.subscribe(testTopic)).expect("Resub failed")
@ -364,7 +364,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 2)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMesh(topicA, "Msg on Shard A".toBytes())).expect(
"Publish A failed"
@ -400,7 +400,7 @@ suite "Messaging API, SubscriptionManager":
# here we just give a chance for any messages that we don't expect to arrive
await sleepAsync(1.seconds)
eventManager.teardown()
await eventManager.teardown()
# weak check (but catches most bugs)
require eventManager.receivedMessages.len == expected.len
@ -451,7 +451,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMeshAfterEdgeReady(testTopic, "Hello, edge!".toBytes())).expect(
"Publish failed"
@ -472,7 +472,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect(
"Publish failed"
@ -493,7 +493,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect(
"Publish failed"
@ -517,7 +517,7 @@ suite "Messaging API, SubscriptionManager":
let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
defer:
eventManager.teardown()
await eventManager.teardown()
net.subscriber.unsubscribe(topicA).expect("failed to unsub A")
@ -546,7 +546,7 @@ suite "Messaging API, SubscriptionManager":
)
require await eventManager.waitForEvents(TestTimeout)
eventManager.teardown()
await eventManager.teardown()
net.subscriber.unsubscribe(testTopic).expect("Unsub failed")
eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
@ -555,7 +555,7 @@ suite "Messaging API, SubscriptionManager":
(await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed")
check not await eventManager.waitForEvents(NegativeTestTimeout)
eventManager.teardown()
await eventManager.teardown()
(await net.subscriber.subscribe(testTopic)).expect("Resub failed")
eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1)
@ -653,7 +653,7 @@ suite "Messaging API, SubscriptionManager":
require await eventManager.waitForEvents(TestTimeout)
check eventManager.receivedMessages[0].payload == "Before failover".toBytes()
eventManager.teardown()
await eventManager.teardown()
# Disconnect meshBuddy from edge (keeps relay mesh alive for publishing)
await subscriber.node.disconnectNode(meshBuddyPeerInfo)
@ -678,7 +678,7 @@ suite "Messaging API, SubscriptionManager":
require await eventManager.waitForEvents(TestTimeout)
check eventManager.receivedMessages[0].payload == "After failover".toBytes()
eventManager.teardown()
await eventManager.teardown()
(await subscriber.stop()).expect("Failed to stop subscriber")
await meshBuddy.stop()
@ -801,7 +801,7 @@ suite "Messaging API, SubscriptionManager":
require await eventManager.waitForEvents(TestTimeout)
check eventManager.receivedMessages[0].payload == "After replacement".toBytes()
eventManager.teardown()
await eventManager.teardown()
(await subscriber.stop()).expect("Failed to stop subscriber")
await sparePeer.stop()

View File

@ -376,6 +376,7 @@ suite "WakuConfBuilder - store retention policies":
test "Multiple retention policies":
## Given
var b = WakuConfBuilder.init()
b.withP2pTcpPort(0'u16)
b.storeServiceConf.withEnabled(true)
b.storeServiceConf.withDbUrl("sqlite://test.db")
b.storeServiceConf.withRetentionPolicies(
@ -420,6 +421,7 @@ suite "WakuConfBuilder - store retention policies":
test "Store disabled - no retention policy applied":
## Given
var b = WakuConfBuilder.init()
b.withP2pTcpPort(0'u16)
# storeServiceConf not enabled
## When

View File

@ -8,7 +8,4 @@ import
./test_parse_size,
./test_requestratelimiter,
./test_ratelimit_setting,
./test_timed_map,
./test_event_broker,
./test_request_broker,
./test_multi_request_broker
./test_timed_map

View File

@ -1,201 +0,0 @@
import chronos
import std/sequtils
import testutils/unittests
import waku/common/broker/event_broker
type ExternalDefinedEventType = object
label*: string
EventBroker:
type IntEvent = int
EventBroker:
type ExternalAliasEvent = distinct ExternalDefinedEventType
EventBroker:
type SampleEvent = object
value*: int
label*: string
EventBroker:
type BinaryEvent = object
flag*: bool
EventBroker:
type RefEvent = ref object
payload*: seq[int]
template waitForListeners() =
waitFor sleepAsync(1.milliseconds)
suite "EventBroker":
test "delivers events to all listeners":
var seen: seq[(int, string)] = @[]
discard SampleEvent.listen(
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
seen.add((evt.value, evt.label))
)
discard SampleEvent.listen(
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
seen.add((evt.value * 2, evt.label & "!"))
)
let evt = SampleEvent(value: 5, label: "hi")
SampleEvent.emit(evt)
waitForListeners()
check seen.len == 2
check seen.anyIt(it == (5, "hi"))
check seen.anyIt(it == (10, "hi!"))
SampleEvent.dropAllListeners()
test "forget removes a single listener":
var counter = 0
let handleA = SampleEvent.listen(
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
inc counter
)
let handleB = SampleEvent.listen(
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
inc(counter, 2)
)
SampleEvent.dropListener(handleA.get())
let eventVal = SampleEvent(value: 1, label: "one")
SampleEvent.emit(eventVal)
waitForListeners()
check counter == 2
SampleEvent.dropAllListeners()
test "forgetAll clears every listener":
var triggered = false
let handle1 = SampleEvent.listen(
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
triggered = true
)
let handle2 = SampleEvent.listen(
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
discard
)
SampleEvent.dropAllListeners()
SampleEvent.emit(42, "noop")
SampleEvent.emit(label = "noop", value = 42)
waitForListeners()
check not triggered
let freshHandle = SampleEvent.listen(
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
discard
)
check freshHandle.get().id > 0'u64
SampleEvent.dropListener(freshHandle.get())
test "broker helpers operate via typedesc":
var toggles: seq[bool] = @[]
let handle = BinaryEvent.listen(
proc(evt: BinaryEvent): Future[void] {.async: (raises: []).} =
toggles.add(evt.flag)
)
BinaryEvent(flag: true).emit()
waitForListeners()
let binaryEvent = BinaryEvent(flag: false)
BinaryEvent.emit(binaryEvent)
waitForListeners()
check toggles == @[true, false]
BinaryEvent.dropAllListeners()
test "ref typed event":
var counter: int = 0
let handle = RefEvent.listen(
proc(evt: RefEvent): Future[void] {.async: (raises: []).} =
for n in evt.payload:
counter += n
)
RefEvent(payload: @[1, 2, 3]).emit()
waitForListeners()
RefEvent.emit(payload = @[4, 5, 6])
waitForListeners()
check counter == 21 # 1+2+3 + 4+5+6
RefEvent.dropAllListeners()
test "supports BrokerContext-scoped listeners":
SampleEvent.dropAllListeners()
let ctxA = NewBrokerContext()
let ctxB = NewBrokerContext()
var seenA: seq[int] = @[]
var seenB: seq[int] = @[]
discard SampleEvent.listen(
ctxA,
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
seenA.add(evt.value),
)
discard SampleEvent.listen(
ctxB,
proc(evt: SampleEvent): Future[void] {.async: (raises: []).} =
seenB.add(evt.value),
)
SampleEvent.emit(ctxA, SampleEvent(value: 1, label: "a"))
SampleEvent.emit(ctxB, SampleEvent(value: 2, label: "b"))
waitForListeners()
check seenA == @[1]
check seenB == @[2]
SampleEvent.dropAllListeners(ctxA)
SampleEvent.emit(ctxA, SampleEvent(value: 3, label: "a2"))
SampleEvent.emit(ctxB, SampleEvent(value: 4, label: "b2"))
waitForListeners()
check seenA == @[1]
check seenB == @[2, 4]
SampleEvent.dropAllListeners(ctxB)
test "supports non-object event types (auto-distinct)":
var seen: seq[int] = @[]
discard IntEvent.listen(
proc(evt: IntEvent): Future[void] {.async: (raises: []).} =
seen.add(int(evt))
)
IntEvent.emit(IntEvent(42))
waitForListeners()
check seen == @[42]
IntEvent.dropAllListeners()
test "supports externally-defined type aliases (auto-distinct)":
var seen: seq[string] = @[]
discard ExternalAliasEvent.listen(
proc(evt: ExternalAliasEvent): Future[void] {.async: (raises: []).} =
let base = ExternalDefinedEventType(evt)
seen.add(base.label)
)
ExternalAliasEvent.emit(ExternalAliasEvent(ExternalDefinedEventType(label: "x")))
waitForListeners()
check seen == @["x"]
ExternalAliasEvent.dropAllListeners()

View File

@ -1,343 +0,0 @@
{.used.}
import testutils/unittests
import chronos
import std/sequtils
import std/strutils
import waku/common/broker/multi_request_broker
MultiRequestBroker:
type NoArgResponse = object
label*: string
proc signatureFetch*(): Future[Result[NoArgResponse, string]] {.async.}
MultiRequestBroker:
type ArgResponse = object
id*: string
proc signatureFetch*(
suffix: string, numsuffix: int
): Future[Result[ArgResponse, string]] {.async.}
MultiRequestBroker:
type DualResponse = ref object
note*: string
suffix*: string
proc signatureBase*(): Future[Result[DualResponse, string]] {.async.}
proc signatureWithInput*(
suffix: string
): Future[Result[DualResponse, string]] {.async.}
type ExternalBaseType = string
MultiRequestBroker:
type NativeIntResponse = int
proc signatureFetch*(): Future[Result[NativeIntResponse, string]] {.async.}
MultiRequestBroker:
type ExternalAliasResponse = ExternalBaseType
proc signatureFetch*(): Future[Result[ExternalAliasResponse, string]] {.async.}
MultiRequestBroker:
type AlreadyDistinctResponse = distinct int
proc signatureFetch*(): Future[Result[AlreadyDistinctResponse, string]] {.async.}
suite "MultiRequestBroker":
test "aggregates zero-argument providers":
discard NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
ok(NoArgResponse(label: "one"))
)
discard NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
discard catch:
await sleepAsync(1.milliseconds)
ok(NoArgResponse(label: "two"))
)
let responses = waitFor NoArgResponse.request()
check responses.get().len == 2
check responses.get().anyIt(it.label == "one")
check responses.get().anyIt(it.label == "two")
NoArgResponse.clearProviders()
test "aggregates argument providers":
discard ArgResponse.setProvider(
proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} =
ok(ArgResponse(id: suffix & "-a-" & $num))
)
discard ArgResponse.setProvider(
proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} =
ok(ArgResponse(id: suffix & "-b-" & $num))
)
let keyed = waitFor ArgResponse.request("topic", 1)
check keyed.get().len == 2
check keyed.get().anyIt(it.id == "topic-a-1")
check keyed.get().anyIt(it.id == "topic-b-1")
ArgResponse.clearProviders()
test "clearProviders resets both provider lists":
discard DualResponse.setProvider(
proc(): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "base", suffix: ""))
)
discard DualResponse.setProvider(
proc(suffix: string): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "base" & suffix, suffix: suffix))
)
let noArgs = waitFor DualResponse.request()
check noArgs.get().len == 1
let param = waitFor DualResponse.request("-extra")
check param.get().len == 1
check param.get()[0].suffix == "-extra"
DualResponse.clearProviders()
let emptyNoArgs = waitFor DualResponse.request()
check emptyNoArgs.get().len == 0
let emptyWithArgs = waitFor DualResponse.request("-extra")
check emptyWithArgs.get().len == 0
test "request returns empty seq when no providers registered":
let empty = waitFor NoArgResponse.request()
check empty.get().len == 0
test "failed providers will fail the request":
NoArgResponse.clearProviders()
discard NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
err("boom")
)
discard NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
ok(NoArgResponse(label: "survivor"))
)
let filtered = waitFor NoArgResponse.request()
check filtered.isErr()
NoArgResponse.clearProviders()
test "deduplicates identical zero-argument providers":
NoArgResponse.clearProviders()
var invocations = 0
let sharedHandler = proc(): Future[Result[NoArgResponse, string]] {.async.} =
inc invocations
ok(NoArgResponse(label: "dup"))
let first = NoArgResponse.setProvider(sharedHandler)
let second = NoArgResponse.setProvider(sharedHandler)
check first.get().id == second.get().id
check first.get().kind == second.get().kind
let dupResponses = waitFor NoArgResponse.request()
check dupResponses.get().len == 1
check invocations == 1
NoArgResponse.clearProviders()
test "removeProvider deletes registered handlers":
var removedCalled = false
var keptCalled = false
let removable = NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
removedCalled = true
ok(NoArgResponse(label: "removed"))
)
discard NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
keptCalled = true
ok(NoArgResponse(label: "kept"))
)
NoArgResponse.removeProvider(removable.get())
let afterRemoval = (waitFor NoArgResponse.request()).valueOr:
assert false, "request failed"
@[]
check afterRemoval.len == 1
check afterRemoval[0].label == "kept"
check not removedCalled
check keptCalled
NoArgResponse.clearProviders()
test "removeProvider works for argument signatures":
var invoked: seq[string] = @[]
discard ArgResponse.setProvider(
proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} =
invoked.add("first" & suffix)
ok(ArgResponse(id: suffix & "-one-" & $num))
)
let handle = ArgResponse.setProvider(
proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} =
invoked.add("second" & suffix)
ok(ArgResponse(id: suffix & "-two-" & $num))
)
ArgResponse.removeProvider(handle.get())
let single = (waitFor ArgResponse.request("topic", 1)).valueOr:
assert false, "request failed"
@[]
check single.len == 1
check single[0].id == "topic-one-1"
check invoked == @["firsttopic"]
ArgResponse.clearProviders()
test "catches exception from providers and report error":
let firstHandler = NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
raise newException(ValueError, "first handler raised")
)
discard NoArgResponse.setProvider(
proc(): Future[Result[NoArgResponse, string]] {.async.} =
ok(NoArgResponse(label: "just ok"))
)
let afterException = waitFor NoArgResponse.request()
check afterException.isErr()
check afterException.error().contains("first handler raised")
NoArgResponse.clearProviders()
test "ref providers returning nil fail request":
DualResponse.clearProviders()
test "supports native request types":
NativeIntResponse.clearProviders()
discard NativeIntResponse.setProvider(
proc(): Future[Result[NativeIntResponse, string]] {.async.} =
ok(NativeIntResponse(1))
)
discard NativeIntResponse.setProvider(
proc(): Future[Result[NativeIntResponse, string]] {.async.} =
ok(NativeIntResponse(2))
)
let res = waitFor NativeIntResponse.request()
check res.isOk()
check res.get().len == 2
check res.get().anyIt(int(it) == 1)
check res.get().anyIt(int(it) == 2)
NativeIntResponse.clearProviders()
test "supports external request types":
ExternalAliasResponse.clearProviders()
discard ExternalAliasResponse.setProvider(
proc(): Future[Result[ExternalAliasResponse, string]] {.async.} =
ok(ExternalAliasResponse("hello"))
)
let res = waitFor ExternalAliasResponse.request()
check res.isOk()
check res.get().len == 1
check ExternalBaseType(res.get()[0]) == "hello"
ExternalAliasResponse.clearProviders()
test "supports already-distinct request types":
AlreadyDistinctResponse.clearProviders()
discard AlreadyDistinctResponse.setProvider(
proc(): Future[Result[AlreadyDistinctResponse, string]] {.async.} =
ok(AlreadyDistinctResponse(7))
)
let res = waitFor AlreadyDistinctResponse.request()
check res.isOk()
check res.get().len == 1
check int(res.get()[0]) == 7
AlreadyDistinctResponse.clearProviders()
test "context-aware providers are isolated":
NoArgResponse.clearProviders()
let ctxA = NewBrokerContext()
let ctxB = NewBrokerContext()
discard NoArgResponse.setProvider(
ctxA,
proc(): Future[Result[NoArgResponse, string]] {.async.} =
ok(NoArgResponse(label: "a")),
)
discard NoArgResponse.setProvider(
ctxB,
proc(): Future[Result[NoArgResponse, string]] {.async.} =
ok(NoArgResponse(label: "b")),
)
let resA = waitFor NoArgResponse.request(ctxA)
check resA.isOk()
check resA.get().len == 1
check resA.get()[0].label == "a"
let resB = waitFor NoArgResponse.request(ctxB)
check resB.isOk()
check resB.get().len == 1
check resB.get()[0].label == "b"
let resDefault = waitFor NoArgResponse.request()
check resDefault.isOk()
check resDefault.get().len == 0
NoArgResponse.clearProviders(ctxA)
let clearedA = waitFor NoArgResponse.request(ctxA)
check clearedA.isOk()
check clearedA.get().len == 0
let stillB = waitFor NoArgResponse.request(ctxB)
check stillB.isOk()
check stillB.get().len == 1
check stillB.get()[0].label == "b"
NoArgResponse.clearProviders(ctxB)
discard DualResponse.setProvider(
proc(): Future[Result[DualResponse, string]] {.async.} =
let nilResponse: DualResponse = nil
ok(nilResponse)
)
let zeroArg = waitFor DualResponse.request()
check zeroArg.isErr()
DualResponse.clearProviders()
discard DualResponse.setProvider(
proc(suffix: string): Future[Result[DualResponse, string]] {.async.} =
let nilResponse: DualResponse = nil
ok(nilResponse)
)
let withInput = waitFor DualResponse.request("-extra")
check withInput.isErr()
DualResponse.clearProviders()

View File

@ -1,675 +0,0 @@
{.used.}
import testutils/unittests
import chronos
import std/strutils
import waku/common/broker/request_broker
## ---------------------------------------------------------------------------
## Async-mode brokers + tests
## ---------------------------------------------------------------------------
RequestBroker:
type SimpleResponse = object
value*: string
proc signatureFetch*(): Future[Result[SimpleResponse, string]] {.async.}
RequestBroker:
type KeyedResponse = object
key*: string
payload*: string
proc signatureFetchWithKey*(
key: string, subKey: int
): Future[Result[KeyedResponse, string]] {.async.}
RequestBroker:
type DualResponse = object
note*: string
count*: int
proc signatureNoInput*(): Future[Result[DualResponse, string]] {.async.}
proc signatureWithInput*(
suffix: string
): Future[Result[DualResponse, string]] {.async.}
RequestBroker(async):
type ImplicitResponse = ref object
note*: string
static:
doAssert typeof(SimpleResponse.request()) is Future[Result[SimpleResponse, string]]
suite "RequestBroker macro (async mode)":
test "serves zero-argument providers":
check SimpleResponse
.setProvider(
proc(): Future[Result[SimpleResponse, string]] {.async.} =
ok(SimpleResponse(value: "hi"))
)
.isOk()
let res = waitFor SimpleResponse.request()
check res.isOk()
check res.value.value == "hi"
SimpleResponse.clearProvider()
test "zero-argument request errors when unset":
let res = waitFor SimpleResponse.request()
check res.isErr()
check res.error.contains("no zero-arg provider")
test "serves input-based providers":
var seen: seq[string] = @[]
check KeyedResponse
.setProvider(
proc(
key: string, subKey: int
): Future[Result[KeyedResponse, string]] {.async.} =
seen.add(key)
ok(KeyedResponse(key: key, payload: key & "-payload+" & $subKey))
)
.isOk()
let res = waitFor KeyedResponse.request("topic", 1)
check res.isOk()
check res.value.key == "topic"
check res.value.payload == "topic-payload+1"
check seen == @["topic"]
KeyedResponse.clearProvider()
test "catches provider exception":
check KeyedResponse
.setProvider(
proc(
key: string, subKey: int
): Future[Result[KeyedResponse, string]] {.async.} =
raise newException(ValueError, "simulated failure")
)
.isOk()
let res = waitFor KeyedResponse.request("neglected", 11)
check res.isErr()
check res.error.contains("simulated failure")
KeyedResponse.clearProvider()
test "input request errors when unset":
let res = waitFor KeyedResponse.request("foo", 2)
check res.isErr()
check res.error.contains("input signature")
test "supports both provider types simultaneously":
check DualResponse
.setProvider(
proc(): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "base", count: 1))
)
.isOk()
check DualResponse
.setProvider(
proc(suffix: string): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "base" & suffix, count: suffix.len))
)
.isOk()
let noInput = waitFor DualResponse.request()
check noInput.isOk()
check noInput.value.note == "base"
let withInput = waitFor DualResponse.request("-extra")
check withInput.isOk()
check withInput.value.note == "base-extra"
check withInput.value.count == 6
DualResponse.clearProvider()
test "clearProvider resets both entries":
check DualResponse
.setProvider(
proc(): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "temp", count: 0))
)
.isOk()
DualResponse.clearProvider()
let res = waitFor DualResponse.request()
check res.isErr()
test "implicit zero-argument provider works by default":
check ImplicitResponse
.setProvider(
proc(): Future[Result[ImplicitResponse, string]] {.async.} =
ok(ImplicitResponse(note: "auto"))
)
.isOk()
let res = waitFor ImplicitResponse.request()
check res.isOk()
ImplicitResponse.clearProvider()
check res.value.note == "auto"
test "implicit zero-argument request errors when unset":
let res = waitFor ImplicitResponse.request()
check res.isErr()
check res.error.contains("no zero-arg provider")
test "no provider override":
check DualResponse
.setProvider(
proc(): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "base", count: 1))
)
.isOk()
check DualResponse
.setProvider(
proc(suffix: string): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "base" & suffix, count: suffix.len))
)
.isOk()
let overrideProc = proc(): Future[Result[DualResponse, string]] {.async.} =
ok(DualResponse(note: "something else", count: 1))
check DualResponse.setProvider(overrideProc).isErr()
let noInput = waitFor DualResponse.request()
check noInput.isOk()
check noInput.value.note == "base"
let stillResponse = waitFor DualResponse.request(" still works")
check stillResponse.isOk()
check stillResponse.value.note.contains("base still works")
DualResponse.clearProvider()
let noResponse = waitFor DualResponse.request()
check noResponse.isErr()
check noResponse.error.contains("no zero-arg provider")
let noResponseArg = waitFor DualResponse.request("Should not work")
check noResponseArg.isErr()
check noResponseArg.error.contains("no provider")
check DualResponse.setProvider(overrideProc).isOk()
let nowSuccWithOverride = waitFor DualResponse.request()
check nowSuccWithOverride.isOk()
check nowSuccWithOverride.value.note == "something else"
check nowSuccWithOverride.value.count == 1
DualResponse.clearProvider()
test "supports keyed providers (async, zero-arg)":
SimpleResponse.clearProvider()
check SimpleResponse
.setProvider(
proc(): Future[Result[SimpleResponse, string]] {.async.} =
ok(SimpleResponse(value: "default"))
)
.isOk()
check SimpleResponse
.setProvider(
BrokerContext(0x11111111'u32),
proc(): Future[Result[SimpleResponse, string]] {.async.} =
ok(SimpleResponse(value: "one")),
)
.isOk()
check SimpleResponse
.setProvider(
BrokerContext(0x22222222'u32),
proc(): Future[Result[SimpleResponse, string]] {.async.} =
ok(SimpleResponse(value: "two")),
)
.isOk()
let defaultRes = waitFor SimpleResponse.request()
check defaultRes.isOk()
check defaultRes.value.value == "default"
let res1 = waitFor SimpleResponse.request(BrokerContext(0x11111111'u32))
check res1.isOk()
check res1.value.value == "one"
let res2 = waitFor SimpleResponse.request(BrokerContext(0x22222222'u32))
check res2.isOk()
check res2.value.value == "two"
let missing = waitFor SimpleResponse.request(BrokerContext(0x33333333'u32))
check missing.isErr()
check missing.error.contains("no provider registered for broker context")
check SimpleResponse
.setProvider(
BrokerContext(0x11111111'u32),
proc(): Future[Result[SimpleResponse, string]] {.async.} =
ok(SimpleResponse(value: "dup")),
)
.isErr()
SimpleResponse.clearProvider()
test "supports keyed providers (async, with args)":
KeyedResponse.clearProvider()
check KeyedResponse
.setProvider(
proc(
key: string, subKey: int
): Future[Result[KeyedResponse, string]] {.async.} =
ok(KeyedResponse(key: "default-" & key, payload: $subKey))
)
.isOk()
check KeyedResponse
.setProvider(
BrokerContext(0xABCDEF01'u32),
proc(
key: string, subKey: int
): Future[Result[KeyedResponse, string]] {.async.} =
ok(KeyedResponse(key: "k1-" & key, payload: "p" & $subKey)),
)
.isOk()
check KeyedResponse
.setProvider(
BrokerContext(0xABCDEF02'u32),
proc(
key: string, subKey: int
): Future[Result[KeyedResponse, string]] {.async.} =
ok(KeyedResponse(key: "k2-" & key, payload: "q" & $subKey)),
)
.isOk()
let d = waitFor KeyedResponse.request("topic", 7)
check d.isOk()
check d.value.key == "default-topic"
let k1 = waitFor KeyedResponse.request(BrokerContext(0xABCDEF01'u32), "topic", 7)
check k1.isOk()
check k1.value.key == "k1-topic"
check k1.value.payload == "p7"
let k2 = waitFor KeyedResponse.request(BrokerContext(0xABCDEF02'u32), "topic", 7)
check k2.isOk()
check k2.value.key == "k2-topic"
check k2.value.payload == "q7"
let miss = waitFor KeyedResponse.request(BrokerContext(0xDEADBEEF'u32), "topic", 7)
check miss.isErr()
check miss.error.contains("no provider registered for broker context")
KeyedResponse.clearProvider()
## ---------------------------------------------------------------------------
## Sync-mode brokers + tests
## ---------------------------------------------------------------------------
RequestBroker(sync):
type SimpleResponseSync = object
value*: string
proc signatureFetch*(): Result[SimpleResponseSync, string]
RequestBroker(sync):
type KeyedResponseSync = object
key*: string
payload*: string
proc signatureFetchWithKey*(
key: string, subKey: int
): Result[KeyedResponseSync, string]
RequestBroker(sync):
type DualResponseSync = object
note*: string
count*: int
proc signatureNoInput*(): Result[DualResponseSync, string]
proc signatureWithInput*(suffix: string): Result[DualResponseSync, string]
RequestBroker(sync):
type ImplicitResponseSync = ref object
note*: string
static:
doAssert typeof(SimpleResponseSync.request()) is Result[SimpleResponseSync, string]
doAssert not (
typeof(SimpleResponseSync.request()) is Future[Result[SimpleResponseSync, string]]
)
doAssert typeof(KeyedResponseSync.request("topic", 1)) is
Result[KeyedResponseSync, string]
suite "RequestBroker macro (sync mode)":
test "serves zero-argument providers (sync)":
check SimpleResponseSync
.setProvider(
proc(): Result[SimpleResponseSync, string] =
ok(SimpleResponseSync(value: "hi"))
)
.isOk()
let res = SimpleResponseSync.request()
check res.isOk()
check res.value.value == "hi"
SimpleResponseSync.clearProvider()
test "zero-argument request errors when unset (sync)":
let res = SimpleResponseSync.request()
check res.isErr()
check res.error.contains("no zero-arg provider")
test "serves input-based providers (sync)":
var seen: seq[string] = @[]
check KeyedResponseSync
.setProvider(
proc(key: string, subKey: int): Result[KeyedResponseSync, string] =
seen.add(key)
ok(KeyedResponseSync(key: key, payload: key & "-payload+" & $subKey))
)
.isOk()
let res = KeyedResponseSync.request("topic", 1)
check res.isOk()
check res.value.key == "topic"
check res.value.payload == "topic-payload+1"
check seen == @["topic"]
KeyedResponseSync.clearProvider()
test "catches provider exception (sync)":
check KeyedResponseSync
.setProvider(
proc(key: string, subKey: int): Result[KeyedResponseSync, string] =
raise newException(ValueError, "simulated failure")
)
.isOk()
let res = KeyedResponseSync.request("neglected", 11)
check res.isErr()
check res.error.contains("simulated failure")
KeyedResponseSync.clearProvider()
test "input request errors when unset (sync)":
let res = KeyedResponseSync.request("foo", 2)
check res.isErr()
check res.error.contains("input signature")
test "supports both provider types simultaneously (sync)":
check DualResponseSync
.setProvider(
proc(): Result[DualResponseSync, string] =
ok(DualResponseSync(note: "base", count: 1))
)
.isOk()
check DualResponseSync
.setProvider(
proc(suffix: string): Result[DualResponseSync, string] =
ok(DualResponseSync(note: "base" & suffix, count: suffix.len))
)
.isOk()
let noInput = DualResponseSync.request()
check noInput.isOk()
check noInput.value.note == "base"
let withInput = DualResponseSync.request("-extra")
check withInput.isOk()
check withInput.value.note == "base-extra"
check withInput.value.count == 6
DualResponseSync.clearProvider()
test "clearProvider resets both entries (sync)":
check DualResponseSync
.setProvider(
proc(): Result[DualResponseSync, string] =
ok(DualResponseSync(note: "temp", count: 0))
)
.isOk()
DualResponseSync.clearProvider()
let res = DualResponseSync.request()
check res.isErr()
test "implicit zero-argument provider works by default (sync)":
check ImplicitResponseSync
.setProvider(
proc(): Result[ImplicitResponseSync, string] =
ok(ImplicitResponseSync(note: "auto"))
)
.isOk()
let res = ImplicitResponseSync.request()
check res.isOk()
ImplicitResponseSync.clearProvider()
check res.value.note == "auto"
test "implicit zero-argument request errors when unset (sync)":
let res = ImplicitResponseSync.request()
check res.isErr()
check res.error.contains("no zero-arg provider")
test "implicit zero-argument provider raises error (sync)":
check ImplicitResponseSync
.setProvider(
proc(): Result[ImplicitResponseSync, string] =
raise newException(ValueError, "simulated failure")
)
.isOk()
let res = ImplicitResponseSync.request()
check res.isErr()
check res.error.contains("simulated failure")
ImplicitResponseSync.clearProvider()
test "supports keyed providers (sync, zero-arg)":
SimpleResponseSync.clearProvider()
check SimpleResponseSync
.setProvider(
proc(): Result[SimpleResponseSync, string] =
ok(SimpleResponseSync(value: "default"))
)
.isOk()
check SimpleResponseSync
.setProvider(
BrokerContext(0x10101010'u32),
proc(): Result[SimpleResponseSync, string] =
ok(SimpleResponseSync(value: "ten")),
)
.isOk()
let defaultRes = SimpleResponseSync.request()
check defaultRes.isOk()
check defaultRes.value.value == "default"
let keyedRes = SimpleResponseSync.request(BrokerContext(0x10101010'u32))
check keyedRes.isOk()
check keyedRes.value.value == "ten"
let miss = SimpleResponseSync.request(BrokerContext(0x20202020'u32))
check miss.isErr()
check miss.error.contains("no provider registered for broker context")
SimpleResponseSync.clearProvider()
test "supports keyed providers (sync, with args)":
KeyedResponseSync.clearProvider()
check KeyedResponseSync
.setProvider(
proc(key: string, subKey: int): Result[KeyedResponseSync, string] =
ok(KeyedResponseSync(key: "default-" & key, payload: $subKey))
)
.isOk()
check KeyedResponseSync
.setProvider(
BrokerContext(0xA0A0A0A0'u32),
proc(key: string, subKey: int): Result[KeyedResponseSync, string] =
ok(KeyedResponseSync(key: "k-" & key, payload: "p" & $subKey)),
)
.isOk()
let d = KeyedResponseSync.request("topic", 2)
check d.isOk()
check d.value.key == "default-topic"
let keyed = KeyedResponseSync.request(BrokerContext(0xA0A0A0A0'u32), "topic", 2)
check keyed.isOk()
check keyed.value.key == "k-topic"
check keyed.value.payload == "p2"
let miss = KeyedResponseSync.request(BrokerContext(0xB0B0B0B0'u32), "topic", 2)
check miss.isErr()
check miss.error.contains("no provider registered for broker context")
KeyedResponseSync.clearProvider()
## ---------------------------------------------------------------------------
## POD / external type brokers + tests (distinct/alias behavior)
## ---------------------------------------------------------------------------
type ExternalDefinedTypeAsync = object
label*: string
type ExternalDefinedTypeSync = object
label*: string
type ExternalDefinedTypeShared = object
label*: string
RequestBroker:
type PodResponse = int
proc signatureFetch*(): Future[Result[PodResponse, string]] {.async.}
RequestBroker:
type ExternalAliasedResponse = ExternalDefinedTypeAsync
proc signatureFetch*(): Future[Result[ExternalAliasedResponse, string]] {.async.}
RequestBroker(sync):
type ExternalAliasedResponseSync = ExternalDefinedTypeSync
proc signatureFetch*(): Result[ExternalAliasedResponseSync, string]
RequestBroker(sync):
type DistinctStringResponseA = distinct string
RequestBroker(sync):
type DistinctStringResponseB = distinct string
RequestBroker(sync):
type ExternalDistinctResponseA = distinct ExternalDefinedTypeShared
RequestBroker(sync):
type ExternalDistinctResponseB = distinct ExternalDefinedTypeShared
suite "RequestBroker macro (POD/external types)":
test "supports non-object response types (async)":
check PodResponse
.setProvider(
proc(): Future[Result[PodResponse, string]] {.async.} =
ok(PodResponse(123))
)
.isOk()
let res = waitFor PodResponse.request()
check res.isOk()
check int(res.value) == 123
PodResponse.clearProvider()
test "supports aliased external types (async)":
check ExternalAliasedResponse
.setProvider(
proc(): Future[Result[ExternalAliasedResponse, string]] {.async.} =
ok(ExternalAliasedResponse(ExternalDefinedTypeAsync(label: "ext")))
)
.isOk()
let res = waitFor ExternalAliasedResponse.request()
check res.isOk()
check ExternalDefinedTypeAsync(res.value).label == "ext"
ExternalAliasedResponse.clearProvider()
test "supports aliased external types (sync)":
check ExternalAliasedResponseSync
.setProvider(
proc(): Result[ExternalAliasedResponseSync, string] =
ok(ExternalAliasedResponseSync(ExternalDefinedTypeSync(label: "ext")))
)
.isOk()
let res = ExternalAliasedResponseSync.request()
check res.isOk()
check ExternalDefinedTypeSync(res.value).label == "ext"
ExternalAliasedResponseSync.clearProvider()
test "distinct response types avoid overload ambiguity (sync)":
check DistinctStringResponseA
.setProvider(
proc(): Result[DistinctStringResponseA, string] =
ok(DistinctStringResponseA("a"))
)
.isOk()
check DistinctStringResponseB
.setProvider(
proc(): Result[DistinctStringResponseB, string] =
ok(DistinctStringResponseB("b"))
)
.isOk()
check ExternalDistinctResponseA
.setProvider(
proc(): Result[ExternalDistinctResponseA, string] =
ok(ExternalDistinctResponseA(ExternalDefinedTypeShared(label: "ea")))
)
.isOk()
check ExternalDistinctResponseB
.setProvider(
proc(): Result[ExternalDistinctResponseB, string] =
ok(ExternalDistinctResponseB(ExternalDefinedTypeShared(label: "eb")))
)
.isOk()
let resA = DistinctStringResponseA.request()
let resB = DistinctStringResponseB.request()
check resA.isOk()
check resB.isOk()
check string(resA.value) == "a"
check string(resB.value) == "b"
let resEA = ExternalDistinctResponseA.request()
let resEB = ExternalDistinctResponseB.request()
check resEA.isOk()
check resEB.isOk()
check ExternalDefinedTypeShared(resEA.value).label == "ea"
check ExternalDefinedTypeShared(resEB.value).label == "eb"
DistinctStringResponseA.clearProvider()
DistinctStringResponseB.clearProvider()
ExternalDistinctResponseA.clearProvider()
ExternalDistinctResponseB.clearProvider()

View File

@ -1,13 +1,23 @@
{.used.}
import testutils/unittests, chronos, libp2p/protocols/connectivity/relay/relay
import
std/[net, options, sequtils, strutils],
testutils/unittests,
chronos,
chronos/transports/[stream, datagram, common],
metrics/chronos_httpserver,
libp2p/[crypto/crypto, multiaddress, protocols/connectivity/relay/relay],
eth/p2p/discoveryv5/enr
import
../testlib/wakunode,
waku/waku_node,
waku/factory/node_factory,
waku/factory/conf_builder/conf_builder,
waku/factory/conf_builder/web_socket_conf_builder
tests/testlib/[wakunode, wakucore],
waku/[waku_node, waku_enr, net/auto_port, discovery/waku_discv5, node/waku_metrics],
waku/factory/[
node_factory,
internal_config,
conf_builder/conf_builder,
conf_builder/web_socket_conf_builder,
]
suite "Node Factory":
asynctest "Set up a node based on default configurations":
@ -38,6 +48,45 @@ suite "Node Factory":
not node.wakuStore.isNil()
not node.wakuArchive.isNil()
test "ENR configuration trims multiaddrs until record fits":
var conf = defaultTestWakuConf()
let bindIp = conf.endpointConf.p2pListenAddress
let bindPort = Port(30303)
let oversizedMultiaddrs = (0 .. 11).mapIt(
MultiAddress
.init(
"/dns4/very-long-logical-hostname-" & $it &
".example.logos.dev.status.im/tcp/30303/wss"
)
.get()
)
let netConfig = NetConfig.init(
clusterId = conf.clusterId,
bindIp = bindIp,
bindPort = bindPort,
extMultiAddrs = oversizedMultiaddrs,
extMultiAddrsOnly = true,
wakuFlags = some(conf.wakuFlags),
).valueOr:
raiseAssert error
let record = enrConfiguration(conf, netConfig).valueOr:
raiseAssert error
let typedRecord = record.toTyped()
require typedRecord.isOk()
let multiaddrsOpt = typedRecord.value.multiaddrs
require multiaddrsOpt.isSome()
let retainedMultiaddrs = multiaddrsOpt.get()
check:
retainedMultiaddrs.len < oversizedMultiaddrs.len
retainedMultiaddrs.len > 0
retainedMultiaddrs == oversizedMultiaddrs[0 ..< retainedMultiaddrs.len]
asynctest "Set up a node with Filter enabled":
var confBuilder = defaultTestWakuConfBuilder()
confBuilder.filterServiceConf.withEnabled(true)
@ -68,5 +117,90 @@ asynctest "Start a node based on default test configuration":
check:
node.started == true
# Default conf has p2pTcpPort=0, so the OS must have assigned a real port.
var hasNonZeroTcp = false
for a in node.switch.peerInfo.listenAddrs:
let s = $a
if ("/tcp/" in s) and not ("/tcp/0" in s):
hasNonZeroTcp = true
check hasNonZeroTcp
## Cleanup
await node.stop()
suite "Auto-port retry":
asynctest "metrics binds on free TCP port, fails on taken":
let takenPort = Port(55100)
let freePort = Port(55101)
let taken = createStreamServer(initTAddress("127.0.0.1", takenPort))
defer:
taken.stop()
await taken.closeWait()
proc buildMetricsConf(port: Port): MetricsServerConf =
var b = MetricsServerConfBuilder.init()
b.withEnabled(true)
b.withHttpPort(port)
b.build().value.get()
let failRes = await startMetricsServerAndLogging(buildMetricsConf(takenPort), 0'u16)
check failRes.isErr()
let okRes = await startMetricsServerAndLogging(buildMetricsConf(freePort), 0'u16)
check okRes.isOk()
if okRes.isOk():
await okRes.get().server.close()
asynctest "discv5 binds on free UDP port, fails on taken":
let takenPort = Port(55200)
let freePort = Port(55201)
proc dummyCb(
transp: DatagramTransport, raddr: TransportAddress
): Future[void] {.async: (raises: []).} =
discard
let takenUdp =
newDatagramTransport(dummyCb, local = initTAddress("0.0.0.0", takenPort))
defer:
await takenUdp.closeWait()
let nodeKey = generateSecp256k1Key()
let node = newTestWakuNode(nodeKey, parseIpAddress("0.0.0.0"), Port(0))
await node.start()
defer:
await node.stop()
proc buildDiscv5Conf(port: Port): Discv5Conf =
var b = Discv5ConfBuilder.init()
b.withEnabled(true)
b.withUdpPort(port)
b.build().value.get()
let failRes = await setupAndStartDiscv5(
node.enr,
node.peerManager,
node.topicSubscriptionQueue,
buildDiscv5Conf(takenPort),
@[],
node.rng,
nodeKey,
parseIpAddress("0.0.0.0"),
0'u16,
)
check failRes.isErr()
let okRes = await setupAndStartDiscv5(
node.enr,
node.peerManager,
node.topicSubscriptionQueue,
buildDiscv5Conf(freePort),
@[],
node.rng,
nodeKey,
parseIpAddress("0.0.0.0"),
0'u16,
)
check okRes.isOk()
if okRes.isOk():
await okRes.get().stop()

View File

@ -4,7 +4,7 @@ import
libp2p/crypto/[crypto, secp],
libp2p/multiaddress,
nimcrypto/utils,
std/[options, random, sequtils],
std/[net, options, random, sequtils],
results,
testutils/unittests
import

View File

@ -2,6 +2,7 @@
import
std/[json, options, sequtils, strutils, tables], testutils/unittests, chronos, results
import brokers/broker_context
import
waku/[
@ -23,7 +24,6 @@ import
events/health_events,
events/peer_events,
waku_archive,
common/broker/broker_context,
]
import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils
@ -277,7 +277,7 @@ suite "Health Monitor - events":
await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()])
let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit)
WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis)
await WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis)
require metadataOk
let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit
@ -380,7 +380,7 @@ suite "Health Monitor - events":
await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()])
let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit)
WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis)
await WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis)
require metadataOk
var deadline = Moment.now() + TestConnectivityTimeLimit
@ -413,7 +413,7 @@ suite "Health Monitor - events":
subMgr.subscribe(contentTopic).expect("Failed to subscribe")
let shardHealthOk = await shardHealthFut.withTimeout(TestConnectivityTimeLimit)
EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis)
await EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis)
check shardHealthOk == true
check subMgr.edgeFilterSubStates.len > 0

View File

@ -0,0 +1,9 @@
{.used.}
import ./test_keys
import ./test_backend
import ./test_lifecycle
import ./test_facade
import ./test_encoding
import ./test_string_lookup
import ./test_singleton

View File

@ -0,0 +1,195 @@
{.used.}
import std/options
import results
import testutils/unittests
import waku/persistency/[types, keys, backend_sqlite]
template str(b: seq[byte]): string =
var s = newString(b.len)
for i, x in b:
s[i] = char(x)
s
proc payload(s: string): seq[byte] =
result = newSeq[byte](s.len)
for i, c in s:
result[i] = byte(c)
suite "Persistency SQLite backend":
test "open in-memory backend and round-trip a single value":
let b = openBackendInMemory().get()
defer:
b.close()
b
.applyOps(
[
TxOp(
category: "msg",
key: key("c1", 1'i64),
kind: txPut,
payload: payload("hello"),
)
]
)
.get()
let got = b.getOne("msg", key("c1", 1'i64)).get()
check got.isSome
check str(got.get) == "hello"
check b.existsOne("msg", key("c1", 1'i64)).get()
check not b.existsOne("msg", key("c1", 2'i64)).get()
test "INSERT OR REPLACE overwrites payload for the same key":
let b = openBackendInMemory().get()
defer:
b.close()
let k = key("c1", 1'i64)
b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("v1"))]).get()
b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("v2"))]).get()
check str(b.getOne("msg", k).get().get) == "v2"
test "deleteOne reports whether the row existed":
let b = openBackendInMemory().get()
defer:
b.close()
let k = key("c1", 1'i64)
check not b.deleteOne("msg", k).get()
b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("x"))]).get()
check b.deleteOne("msg", k).get()
check not b.existsOne("msg", k).get()
test "applyOps batches multiple ops atomically":
let b = openBackendInMemory().get()
defer:
b.close()
b
.applyOps(
[
TxOp(
category: "msg", key: key("c1", 1'i64), kind: txPut, payload: payload("a")
),
TxOp(
category: "msg", key: key("c1", 2'i64), kind: txPut, payload: payload("b")
),
TxOp(
category: "msg", key: key("c1", 3'i64), kind: txPut, payload: payload("c")
),
]
)
.get()
check b.countRange("msg", prefixRange(key("c1"))).get() == 3
test "scanRange ascending yields rows in key order":
let b = openBackendInMemory().get()
defer:
b.close()
let inserts = @[5'i64, 1, 4, 2, 3]
var ops: seq[TxOp] = @[]
for i in inserts:
ops.add(
TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))
)
b.applyOps(ops).get()
let rows = b.scanRange("msg", prefixRange(key("c1"))).get()
check rows.len == 5
var seenOrder: seq[string]
for r in rows:
seenOrder.add(str(r.payload))
check seenOrder == @["1", "2", "3", "4", "5"]
test "scanRange descending yields rows in reverse key order":
let b = openBackendInMemory().get()
defer:
b.close()
for i in [1'i64, 2, 3]:
b
.applyOps(
[TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))]
)
.get()
let rows = b.scanRange("msg", prefixRange(key("c1")), reverse = true).get()
check rows.len == 3
check str(rows[0].payload) == "3"
check str(rows[2].payload) == "1"
test "scanRange respects half-open [start, stop) bounds":
let b = openBackendInMemory().get()
defer:
b.close()
for i in [1'i64, 2, 3, 4, 5]:
b
.applyOps(
[TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))]
)
.get()
let rng = KeyRange(start: key("c1", 2'i64), stop: key("c1", 4'i64))
let rows = b.scanRange("msg", rng).get()
check rows.len == 2 # 2 and 3, not 4
check str(rows[0].payload) == "2"
check str(rows[1].payload) == "3"
test "scanRange with empty stop is open-ended":
let b = openBackendInMemory().get()
defer:
b.close()
for i in [1'i64, 2, 3]:
b
.applyOps(
[TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))]
)
.get()
let rng = KeyRange(start: key("c1", 2'i64), stop: rawKey(@[]))
let rows = b.scanRange("msg", rng).get()
check rows.len == 2
check str(rows[1].payload) == "3"
test "categories isolate keyspaces":
let b = openBackendInMemory().get()
defer:
b.close()
let k = key("c1", 1'i64)
b
.applyOps(
[
TxOp(category: "log", key: k, kind: txPut, payload: payload("log-1")),
TxOp(
category: "outgoing", key: k, kind: txPut, payload: payload("outgoing-1")
),
]
)
.get()
check str(b.getOne("log", k).get().get) == "log-1"
check str(b.getOne("outgoing", k).get().get) == "outgoing-1"
check b.countRange("log", prefixRange(key("c1"))).get() == 1
check b.countRange("outgoing", prefixRange(key("c1"))).get() == 1
test "txDelete inside a batch removes the row":
let b = openBackendInMemory().get()
defer:
b.close()
let k = key("c1", 1'i64)
b
.applyOps(
[
TxOp(category: "msg", key: k, kind: txPut, payload: payload("v")),
TxOp(category: "msg", key: k, kind: txDelete),
]
)
.get()
check not b.existsOne("msg", k).get()
test "missing key returns none":
let b = openBackendInMemory().get()
defer:
b.close()
check b.getOne("msg", key("nope")).get().isNone
test "countRange of empty category is zero":
let b = openBackendInMemory().get()
defer:
b.close()
check b.countRange("msg", prefixRange(key("c1"))).get() == 0

View File

@ -0,0 +1,154 @@
{.used.}
import std/[algorithm, options, os, times]
import chronos, results
import testutils/unittests
import waku/persistency/persistency
# Reusable byte-wise comparator (Key has its own `<`, but we sometimes
# want to sort `seq[Key]` here without relying on it for double-checking).
proc cmpBytes(a, b: Key): int =
let ab = bytes(a)
let bb = bytes(b)
let n = min(ab.len, bb.len)
for i in 0 ..< n:
if ab[i] != bb[i]:
return cmp(ab[i], bb[i])
cmp(ab.len, bb.len)
template str(b: seq[byte]): string =
var s = newString(b.len)
for i, x in b:
s[i] = char(x)
s
# Shared payload types used by multiple tests.
type
Mood = enum
moodCalm
moodHappy
moodAngry
Header = object
sender: string
epoch: int64
Msg = object
header: Header
mood: Mood
body: seq[byte]
suite "Persistency generic encoding":
# ── Key macro: composite types ────────────────────────────────────────
test "key macro accepts plain tuples":
let k1 = key(("ch", 1'i64))
let k2 = key("ch", 1'i64)
# A plain tuple is encoded field-by-field, so the result is identical
# to passing the fields directly.
check k1 == k2
test "key macro accepts named tuples":
type Coord = tuple[lane: string, seqNum: int64]
let k = key((lane: "a", seqNum: 7'i64))
let kFlat = key("a", 7'i64)
check k == kFlat
test "key macro accepts a user object":
let k1 = key(Header(sender: "alice", epoch: 5'i64))
let k2 = key("alice", 5'i64)
check k1 == k2
test "key macro accepts nested object inside another arg":
let k1 = key("v1", Header(sender: "alice", epoch: 5'i64))
let k2 = key("v1", "alice", 5'i64)
check k1 == k2
test "key macro encodes enums":
let k1 = key(moodAngry)
let k2 = key(int64(ord(moodAngry)))
check k1 == k2
test "toKey is equivalent to single-arg key()":
check toKey("x") == key("x")
check toKey(42'i64) == key(42'i64)
check toKey(Header(sender: "a", epoch: 1)) == key("a", 1'i64)
test "tuple-encoded keys preserve field-major sort order":
let inputs = @[
key(("a", 0'i64)),
key(("a", 1'i64)),
key(("a", int64.high)),
key(("b", int64.low)),
key(("b", 0'i64)),
]
var shuffled = @[inputs[3], inputs[0], inputs[4], inputs[2], inputs[1]]
shuffled.sort(cmpBytes)
check shuffled == inputs
test "embedded Key encodes verbatim":
let inner = key("a", 7'i64)
let outer = key("prefix", inner)
# Expanded: bytes of "prefix" + raw bytes of inner.
let expanded = key("prefix", "a", 7'i64)
check outer == expanded
# ── Payload macro / toPayload ─────────────────────────────────────────
test "toPayload encodes primitives":
check str(toPayload("hi")).len == 4 # 2-byte len prefix + 2 chars
check toPayload(42'i64).len == 8
check toPayload(true) == @[1'u8]
check toPayload(false) == @[0'u8]
test "toPayload encodes objects field-by-field":
let m = Msg(
header: Header(sender: "alice", epoch: 9'i64),
mood: moodHappy,
body: @[0xAA'u8, 0xBB, 0xCC],
)
let p = toPayload(m)
let pManual = payload("alice", 9'i64, int64(ord(moodHappy)), @[0xAA'u8, 0xBB, 0xCC])
check p == pManual
test "payload macro concatenates parts":
let p = payload("v1", 1'i64, @[0xDE'u8, 0xAD])
# Same as building each piece separately.
var expected: seq[byte] = @[]
encodePart(expected, "v1")
encodePart(expected, 1'i64)
encodePart(expected, @[0xDE'u8, 0xAD])
check p == expected
# ── End-to-end through the facade ─────────────────────────────────────
asyncTest "persistEncoded round-trips a struct through SQLite":
let root = getTempDir() / ("persistency_enc_" & $epochTime().int)
removeDir(root)
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let job = p.openJob("t").get()
let m = Msg(
header: Header(sender: "alice", epoch: 1'i64),
mood: moodHappy,
body: @[1'u8, 2, 3],
)
let k = key("channel-42", m.header.epoch)
await job.persistEncoded("msg", k, m)
# Poll for the row, then read it back as raw bytes.
let deadline = epochTime() + 1.0
var got: Option[seq[byte]]
while epochTime() < deadline:
let r = await job.get("msg", k)
check r.isOk
got = r.get()
if got.isSome:
break
await sleepAsync(chronos.milliseconds(2))
check got.isSome
check got.get == toPayload(m)

View File

@ -0,0 +1,196 @@
{.used.}
import std/[options, os, strutils, times]
import chronos, results
import testutils/unittests
import waku/persistency/persistency
proc payload(s: string): seq[byte] =
result = newSeq[byte](s.len)
for i, c in s:
result[i] = byte(c)
template str(b: seq[byte]): string =
var s = newString(b.len)
for i, x in b:
s[i] = char(x)
s
proc tmpRoot(label: string): string =
let p = getTempDir() / ("persistency_facade_" & label & "_" & $epochTime().int)
removeDir(p)
p
# Bounded poll on exists() to bridge the documented persist->read race.
proc waitUntilExists(
t: Job, category: string, k: Key, timeoutMs = 1000
): Future[bool] {.async.} =
let deadline = epochTime() + (timeoutMs.float / 1000.0)
while epochTime() < deadline:
let r = await t.exists(category, k)
if r.isOk and r.get():
return true
await sleepAsync(chronos.milliseconds(2))
return false
suite "Persistency facade":
asyncTest "persistPut then get round-trips":
let root = tmpRoot("put_get")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
let k = key("c", 1'i64)
await t.persistPut("msg", k, payload("hi"))
let ckOk1 = await t.waitUntilExists("msg", k)
check ckOk1
let aw1 = await t.get("msg", k)
let got = aw1.get()
check got.isSome
check str(got.get) == "hi"
asyncTest "persist (batch) is atomic and visible together":
let root = tmpRoot("batch")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
var ops: seq[TxOp]
for i in 1'i64 .. 4:
ops.add(
TxOp(category: "msg", key: key("c", i), kind: txPut, payload: payload($i))
)
await t.persist(ops)
let ckOk2 = await t.waitUntilExists("msg", key("c", 4'i64))
check ckOk2
let aw2 = await t.count("msg", prefixRange(key("c")))
let cnt = aw2.get()
check cnt == 4
asyncTest "scanPrefix returns rows in key order":
let root = tmpRoot("scan")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
for i in [3'i64, 1, 4, 1, 5, 9, 2]:
await t.persistPut("msg", key("c", i), payload($i))
let ckOk3 = await t.waitUntilExists("msg", key("c", 9'i64))
check ckOk3
let aw3 = await t.scanPrefix("msg", key("c"))
let rows = aw3.get()
# 7 ops with duplicate key i=1 -> 6 distinct rows
check rows.len == 6
var seenOrder: seq[int]
for r in rows:
seenOrder.add(parseInt(str(r.payload)))
check seenOrder == @[1, 2, 3, 4, 5, 9]
asyncTest "scanPrefix reverse=true returns rows in reverse order":
let root = tmpRoot("scan_rev")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
for i in 1'i64 .. 3:
await t.persistPut("msg", key("c", i), payload($i))
let ckOk4 = await t.waitUntilExists("msg", key("c", 3'i64))
check ckOk4
let aw4 = await t.scanPrefix("msg", key("c"), reverse = true)
let rows = aw4.get()
check rows.len == 3
check str(rows[0].payload) == "3"
check str(rows[2].payload) == "1"
asyncTest "deleteAcked round-trips and reports row presence":
let root = tmpRoot("delete")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
let k = key("c", 1'i64)
let aw5 = await t.deleteAcked("msg", k)
let miss = aw5.get()
check miss == false
await t.persistPut("msg", k, payload("v"))
let ckOk5 = await t.waitUntilExists("msg", k)
check ckOk5
let aw6 = await t.deleteAcked("msg", k)
let hit = aw6.get()
check hit == true
let aw7 = await t.exists("msg", k)
check aw7.get() == false
asyncTest "persistDelete fire-and-forget removes the row":
let root = tmpRoot("fadel")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
let k = key("c", 1'i64)
await t.persistPut("msg", k, payload("v"))
let ckOk6 = await t.waitUntilExists("msg", k)
check ckOk6
await t.persistDelete("msg", k)
# Poll for absence.
let deadline = epochTime() + 1.0
var gone = false
while epochTime() < deadline:
let aw8 = await t.exists("msg", k)
if not aw8.get():
gone = true
break
await sleepAsync(chronos.milliseconds(2))
check gone
asyncTest "two jobs do not see each other's data via the facade":
let root = tmpRoot("iso")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let a = p.openJob("a").get()
let b = p.openJob("b").get()
let k = key("c", 1'i64)
await a.persistPut("msg", k, payload("A"))
await b.persistPut("msg", k, payload("B"))
let ckOk7 = await a.waitUntilExists("msg", k)
check ckOk7
let ckOk8 = await b.waitUntilExists("msg", k)
check ckOk8
let aw9 = await a.get("msg", k)
check str(aw9.get().get) == "A"
let aw10 = await b.get("msg", k)
check str(aw10.get().get) == "B"
let aw11 = await a.count("msg", prefixRange(key("c")))
check aw11.get() == 1
let aw12 = await b.count("msg", prefixRange(key("c")))
check aw12.get() == 1

View File

@ -0,0 +1,135 @@
{.used.}
import std/[algorithm, sequtils]
import testutils/unittests
import waku/persistency/[types, keys]
proc cmpBytes(a, b: Key): int =
let ab = bytes(a)
let bb = bytes(b)
let n = min(ab.len, bb.len)
for i in 0 ..< n:
if ab[i] != bb[i]:
return cmp(ab[i], bb[i])
cmp(ab.len, bb.len)
suite "Persistency keys":
test "string components sort by length, then byte order":
var ks = @[key("ab"), key(""), key("a"), key("aa"), key("b")]
ks.sort(cmpBytes)
# length-prefix encoding => shorter strings always sort before longer
# ones; same-length strings sort in byte order.
check ks == @[key(""), key("a"), key("b"), key("aa"), key("ab")]
test "same-length strings sort in byte order":
var ks = @[key("delta"), key("alpha"), key("gamma"), key("bravo")]
ks.sort(cmpBytes)
check ks == @[key("alpha"), key("bravo"), key("delta"), key("gamma")]
test "int64 sign-flip preserves order across negative/zero/positive":
let inputs = @[
key("c", int64.low),
key("c", -2'i64),
key("c", -1'i64),
key("c", 0'i64),
key("c", 1'i64),
key("c", 2'i64),
key("c", int64.high),
]
var shuffled = inputs
# rotate so the natural order is not the input order
shuffled = @[
shuffled[3],
shuffled[6],
shuffled[0],
shuffled[5],
shuffled[1],
shuffled[4],
shuffled[2],
]
shuffled.sort(cmpBytes)
check shuffled == inputs
test "uint64 big-endian preserves order":
let inputs = @[
key("u", 0'u64),
key("u", 1'u64),
key("u", 256'u64),
key("u", 1_000_000'u64),
key("u", uint64.high - 1),
key("u", uint64.high),
]
var shuffled = @[inputs[3], inputs[0], inputs[5], inputs[2], inputs[1], inputs[4]]
shuffled.sort(cmpBytes)
check shuffled == inputs
test "composite (string, string) tuple ordering":
# First component "a" / "b" — both length 1, so byte order applies.
# Second components grouped by first; within each group, again
# length-then-byte: "" (len 0) < "a","z" (len 1) < "ab" (len 2).
let inputs = @[
key("a", ""),
key("a", "a"),
key("a", "z"),
key("a", "ab"),
key("b", ""),
key("b", "a"),
]
var shuffled = inputs.reversed()
shuffled.sort(cmpBytes)
check shuffled == inputs
test "composite (string, int64) tuple ordering":
let inputs = @[
key("a", int64.low),
key("a", -1'i64),
key("a", 0'i64),
key("a", 1'i64),
key("b", int64.low),
key("b", 0'i64),
]
var shuffled = inputs.reversed()
shuffled.sort(cmpBytes)
check shuffled == inputs
test "shorter composite key precedes longer one sharing its prefix":
check key("a") < key("a", 0'i64)
check key("a") < key("a", "")
check key("a", "x") < key("a", "x", "y")
test "Key equality is byte-wise":
check key("a", 1'i64) == key("a", 1'i64)
check not (key("a", 1'i64) == key("a", 2'i64))
test "prefixRange.start equals prefix":
let r = prefixRange(key("a"))
check r.start == key("a")
test "prefixRange.stop excludes the prefix and admits all extensions":
let r = prefixRange(key("a"))
let extensions = @[
key("a"),
key("a", 0'i64),
key("a", int64.high),
key("a", "x"),
key("a", uint64.high),
]
for k in extensions:
check r.start <= k
check k < r.stop
test "prefixRange.stop excludes siblings outside the prefix":
let r = prefixRange(key("a"))
# "b" has the same encoded length as "a" but a higher last byte, so it
# should be at-or-above the exclusive stop.
check not (key("b") < r.stop)
# "ab" has more bytes — its 2-byte length prefix bumps it past stop.
check not (key("ab") < r.stop)
# The empty key sits before the start.
check key("") < r.start
test "prefixRange handles all-0xFF prefix as open-ended":
let prefix = rawKey(@[0xFF'u8, 0xFF, 0xFF])
let r = prefixRange(prefix)
check r.start == prefix
check bytes(r.stop).len == 0

View File

@ -0,0 +1,302 @@
{.used.}
import std/[options, os, times]
import chronos, results
import testutils/unittests
import brokers/[event_broker, request_broker]
import waku/persistency/persistency
import waku/persistency/backend_comm
proc payloadBytes(s: string): seq[byte] =
result = newSeq[byte](s.len)
for i, c in s:
result[i] = byte(c)
template str(b: seq[byte]): string =
var s = newString(b.len)
for i, x in b:
s[i] = char(x)
s
proc tmpRoot(label: string): string =
let p = getTempDir() / ("persistency_test_" & label & "_" & $epochTime().int)
removeDir(p)
p
# Cross-thread persist: emit a PersistEvent then poll until the row shows up
# via KvExists. The PersistEvent listener is fire-and-forget, so reads
# immediately after emit are racy by design (documented in v1).
proc pollExists(
t: Job, category: string, k: Key, timeoutMs = 1000
): Future[bool] {.async.} =
let deadline = epochTime() + (timeoutMs.float / 1000.0)
while epochTime() < deadline:
let r = await KvExists.request(t.context, category, k)
if r.isOk and r.get().value:
return true
await sleepAsync(chronos.milliseconds(2))
return false
suite "Persistency lifecycle":
test "Persistency.instance accepts a pre-existing rootDir":
let root = tmpRoot("preexisting")
defer:
removeDir(root)
createDir(root) # pretend a previous run left it
let marker = root / "do-not-touch.txt"
writeFile(marker, "hi")
defer:
removeFile(marker)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
# The pre-existing file is untouched.
check fileExists(marker)
check readFile(marker) == "hi"
test "Persistency.instance refuses a non-directory path":
let root = tmpRoot("collision")
defer:
removeFile(root)
writeFile(root, "im a file not a dir") # collide with rootDir name
let r = Persistency.instance(root)
check r.isErr
check r.error.kind == peInvalidArgument
test "Persistency.instance defers rootDir creation until first openJob":
let root = tmpRoot("lazy")
defer:
removeDir(root)
check not dirExists(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
# instance() must not have touched the filesystem
check not dirExists(root)
discard p.openJob("first").get()
# first openJob materialises the directory
check dirExists(root)
test "Persistency.instance refuses a path whose ancestor is not a directory":
let parent = tmpRoot("bad-parent")
defer:
removeFile(parent)
writeFile(parent, "not a directory")
let root = parent / "child"
let r = Persistency.instance(root)
check r.isErr
check r.error.kind == peInvalidArgument
asyncTest "openJob reuses an existing DB file across processes-of-one":
let root = tmpRoot("reopen")
defer:
removeDir(root)
# First "session": write something then close.
block firstSession:
let p = Persistency.instance(root).get()
let j = p.openJob("persist").get()
await j.persistPut("msg", key("c", 1'i64), payloadBytes("v1"))
let ckOk1 = await j.pollExists("msg", key("c", 1'i64))
check ckOk1
Persistency.reset()
check fileExists(root / "persist.db")
# Second "session": reopen and read the data back.
block secondSession:
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let j = p.openJob("persist").get()
let aw1 = await KvGet.request(j.context, "msg", key("c", 1'i64))
let got = aw1.get()
check got.value.isSome
check str(got.value.get) == "v1"
test "openJob is idempotent within a session":
let root = tmpRoot("idem")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let a = p.openJob("same").get()
let b = p.openJob("same").get()
check a.id == b.id
check a.context == b.context
test "openJob materialises rootDir and launches a worker":
let root = tmpRoot("basic")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("alpha").get()
check t.id == "alpha"
check t.running
check fileExists(root / "alpha.db")
asyncTest "persist then read round-trips via brokers":
let root = tmpRoot("rw")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t1").get()
let k = key("c", 1'i64)
let ev = PersistEvent(
ops: @[TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("hello"))]
)
await PersistEvent.emit(t.context, ev)
let ckOk2 = await t.pollExists("msg", k)
check ckOk2
let aw2 = await KvGet.request(t.context, "msg", k)
let got = aw2.get()
check got.value.isSome
check str(got.value.get) == "hello"
asyncTest "two jobs run in parallel with isolated DBs":
let root = tmpRoot("isolation")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let a = p.openJob("alpha").get()
let b = p.openJob("beta").get()
check a.context != b.context
let k = key("shared", 1'i64)
await PersistEvent.emit(
a.context,
PersistEvent(
ops: @[
TxOp(
category: "msg", key: k, kind: txPut, payload: payloadBytes("from-alpha")
)
]
),
)
await PersistEvent.emit(
b.context,
PersistEvent(
ops: @[
TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("from-beta"))
]
),
)
let ckOk3 = await a.pollExists("msg", k)
check ckOk3
let ckOk4 = await b.pollExists("msg", k)
check ckOk4
let aw3 = await KvGet.request(a.context, "msg", k)
let aGot = aw3.get()
let aw4 = await KvGet.request(b.context, "msg", k)
let bGot = aw4.get()
check str(aGot.value.get) == "from-alpha"
check str(bGot.value.get) == "from-beta"
# Each job has its own DB file.
check fileExists(root / "alpha.db")
check fileExists(root / "beta.db")
asyncTest "closeJob joins the worker and frees the slot":
let root = tmpRoot("close")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("x").get()
let ctx = t.context
p.closeJob("x")
check not t.running
# After close, requests on the old context have no provider.
let r = await KvExists.request(ctx, "msg", key("k"))
check r.isErr
test "dropJob removes the DB file":
let root = tmpRoot("drop")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
discard p.openJob("ephemeral").get()
check fileExists(root / "ephemeral.db")
p.dropJob("ephemeral")
check not fileExists(root / "ephemeral.db")
asyncTest "scan and count over a range":
let root = tmpRoot("scan")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
var ops: seq[TxOp]
for i in 1'i64 .. 5:
ops.add(
TxOp(category: "msg", key: key("c", i), kind: txPut, payload: payloadBytes($i))
)
await PersistEvent.emit(t.context, PersistEvent(ops: ops))
# Wait for the last insert to land.
let ckOk5 = await t.pollExists("msg", key("c", 5'i64))
check ckOk5
let rng = prefixRange(key("c"))
let aw5 = await KvCount.request(t.context, "msg", rng)
let cnt = aw5.get()
check cnt.n == 5
let aw6 = await KvScan.request(t.context, "msg", rng, false)
let scn = aw6.get()
check scn.rows.len == 5
check str(scn.rows[0].payload) == "1"
check str(scn.rows[4].payload) == "5"
asyncTest "acked delete reports whether the row existed":
let root = tmpRoot("delete")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let t = p.openJob("t").get()
let k = key("d", 1'i64)
let aw7 = await KvDelete.request(t.context, "msg", k)
let r1 = aw7.get()
check r1.existed == false
await PersistEvent.emit(
t.context,
PersistEvent(
ops: @[TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("v"))]
),
)
let ckOk6 = await t.pollExists("msg", k)
check ckOk6
let aw8 = await KvDelete.request(t.context, "msg", k)
let r2 = aw8.get()
check r2.existed == true
let aw9 = await KvExists.request(t.context, "msg", k)
let r3 = aw9.get()
check r3.value == false

View File

@ -0,0 +1,79 @@
{.used.}
import std/[os, strutils, times]
import chronos, results
import testutils/unittests
import brokers/multi_request_broker
import waku/persistency/persistency
proc tmpRoot(label: string): string =
let p = getTempDir() / ("persistency_singleton_" & label & "_" & $epochTime().int)
removeDir(p)
p
suite "Persistency singleton":
test "instance(rootDir) is idempotent with the same rootDir":
let root = tmpRoot("idem")
defer:
removeDir(root)
defer:
Persistency.reset()
let p1 = Persistency.instance(root).get()
let p2 = Persistency.instance(root).get()
check p1 == p2
test "instance(rootDir) refuses re-init with a different rootDir":
let rootA = tmpRoot("a")
let rootB = tmpRoot("b")
defer:
removeDir(rootA)
defer:
removeDir(rootB)
defer:
Persistency.reset()
discard Persistency.instance(rootA).get()
let r = Persistency.instance(rootB)
check r.isErr
check r.error.kind == peInvalidArgument
test "no-arg instance() fails before init, succeeds after":
let root = tmpRoot("noarg")
defer:
removeDir(root)
defer:
Persistency.reset()
let before = Persistency.instance()
check before.isErr
check before.error.kind == peClosed
discard Persistency.instance(root).get()
let after = Persistency.instance()
check after.isOk
test "reset() makes the next instance() target a different rootDir":
let rootA = tmpRoot("rs-a")
let rootB = tmpRoot("rs-b")
defer:
removeDir(rootA)
defer:
removeDir(rootB)
defer:
Persistency.reset()
let pA = Persistency.instance(rootA).get()
check pA.rootDir == rootA
Persistency.reset()
let pB = Persistency.instance(rootB).get()
check pB.rootDir == rootB
check pA != pB
test "reset() is idempotent":
defer:
Persistency.reset()
Persistency.reset()
Persistency.reset()
check Persistency.instance().isErr

View File

@ -0,0 +1,184 @@
{.used.}
import std/[options, os, times]
import chronos, results
import testutils/unittests
import waku/persistency/persistency
proc payloadBytes(s: string): seq[byte] =
result = newSeq[byte](s.len)
for i, c in s:
result[i] = byte(c)
template str(b: seq[byte]): string =
var s = newString(b.len)
for i, x in b:
s[i] = char(x)
s
proc tmpRoot(label: string): string =
let p = getTempDir() / ("persistency_lookup_" & label & "_" & $epochTime().int)
removeDir(p)
p
# Bridge the persist->read race (writes are fire-and-forget in v1).
proc waitUntilExists(
p: Persistency, jobId, category: string, k: Key, timeoutMs = 1000
): Future[bool] {.async.} =
let deadline = epochTime() + (timeoutMs.float / 1000.0)
while epochTime() < deadline:
let r = await p.exists(jobId, category, k)
if r.isOk and r.get():
return true
await sleepAsync(chronos.milliseconds(2))
return false
suite "Persistency string-id lookup":
test "job(p, id) returns peJobNotFound when not open":
let root = tmpRoot("notfound")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let r = p.job("nope")
check r.isErr
check r.error.kind == peJobNotFound
test "job(p, id) returns the Job after openJob":
let root = tmpRoot("found")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let opened = p.openJob("alpha").get()
let looked = p.job("alpha").get()
check looked.id == "alpha"
check looked == opened # same ref, no need to peek at .context
test "hasJob mirrors p.job()":
let root = tmpRoot("has")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
check not p.hasJob("x")
discard p.openJob("x")
check p.hasJob("x")
p.closeJob("x")
check not p.hasJob("x")
test "subscript [] returns the open Job":
let root = tmpRoot("subscript")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
discard p.openJob("a").get()
let j = p["a"]
check j.id == "a"
asyncTest "string-lookup persistPut + get round-trips without a Job ref":
let root = tmpRoot("rw")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
discard p.openJob("svc").get()
let k = key("c", 1'i64)
await p.persistPut("svc", "msg", k, payloadBytes("hello"))
let ckOk1 = await p.waitUntilExists("svc", "msg", k)
check ckOk1
let aw1 = await p.get("svc", "msg", k)
let got = aw1.get()
check got.isSome
check str(got.get) == "hello"
asyncTest "string-lookup reads short-circuit with peJobNotFound":
let root = tmpRoot("missingread")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let g = await p.get("nope", "msg", key("k"))
check g.isErr
check g.error.kind == peJobNotFound
let c = await p.count("nope", "msg", prefixRange(key("k")))
check c.isErr
check c.error.kind == peJobNotFound
let d = await p.deleteAcked("nope", "msg", key("k"))
check d.isErr
check d.error.kind == peJobNotFound
asyncTest "string-lookup writes to an unknown job are dropped, not raised":
let root = tmpRoot("missingwrite")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
# Should not raise and should not leak any state.
await p.persistPut("ghost", "msg", key("k"), payloadBytes("v"))
await p.persistDelete("ghost", "msg", key("k"))
await p.persistEncoded("ghost", "msg", key("k"), 42'i64)
check not p.hasJob("ghost")
asyncTest "string-lookup persistEncoded round-trips a struct":
let root = tmpRoot("encoded")
defer:
removeDir(root)
type Item = object
tag: string
n: int64
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
discard p.openJob("e").get()
let k = key("items", 1'i64)
await p.persistEncoded("e", "msg", k, Item(tag: "alpha", n: 7))
let ckOk2 = await p.waitUntilExists("e", "msg", k)
check ckOk2
let aw2 = await p.get("e", "msg", k)
let got = aw2.get()
check got.isSome
check got.get == toPayload(Item(tag: "alpha", n: 7))
asyncTest "string-lookup scan returns the same rows as Job-form":
let root = tmpRoot("scan")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let j = p.openJob("s").get()
for i in 1'i64 .. 3:
await p.persistPut("s", "msg", key("c", i), payloadBytes($i))
let ckOk3 = await p.waitUntilExists("s", "msg", key("c", 3'i64))
check ckOk3
let aw3 = await p.scanPrefix("s", "msg", key("c"))
let viaId = aw3.get()
let aw4 = await j.scanPrefix("msg", key("c"))
let viaRef = aw4.get()
check viaId.len == viaRef.len
for i in 0 ..< viaId.len:
check viaId[i].key == viaRef[i].key
check viaId[i].payload == viaRef[i].payload

View File

@ -54,6 +54,44 @@ procSuite "Peer Manager":
nodes[0].peerManager.switch.peerStore.connectedness(nodes[1].peerInfo.peerId) ==
Connectedness.Connected
asyncTest "Peer manager tracks active store request state":
let nodes = toSeq(0 ..< 2).mapIt(
newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0))
)
await allFutures(nodes.mapIt(it.start()))
await allFutures(nodes.mapIt(it.mountRelay()))
let peerId = nodes[1].peerInfo.peerId
require (
await nodes[0].peerManager.connectPeer(nodes[1].peerInfo.toRemotePeerInfo())
)
await sleepAsync(chronos.milliseconds(500))
nodes[0].peerManager.addActiveStoreRequest(peerId)
check:
nodes[0].peerManager.hasActiveStoreRequest(peerId)
await nodes[0].peerManager.evictPeer(peerId)
await sleepAsync(chronos.milliseconds(100))
check:
nodes[0].peerManager.switch.peerStore.connectedness(peerId) ==
Connectedness.Connected
nodes[0].peerManager.removeActiveStoreRequest(peerId)
check:
not nodes[0].peerManager.hasActiveStoreRequest(peerId)
await nodes[0].peerManager.evictPeer(peerId)
await sleepAsync(chronos.milliseconds(100))
check:
nodes[0].peerManager.switch.peerStore.connectedness(peerId) !=
Connectedness.Connected
await allFutures(nodes.mapIt(it.stop()))
asyncTest "dialPeer() works":
# Create 2 nodes
let nodes = toSeq(0 ..< 2).mapIt(

View File

@ -271,6 +271,44 @@ suite "Waku ENR - Multiaddresses":
multiaddrs.contains(expectedAddr1)
multiaddrs.contains(addr2)
test "encode and decode record with multiaddrs field deduplicates duplicate entries":
## Given
let
enrSeqNum = 1u64
enrPrivKey = generatesecp256k1key()
let
addr1 = MultiAddress
.init(
"/ip4/127.0.0.1/tcp/80/ws/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr31iDQpSN5Qa882BCjjwgrD"
)
.get()
addr1NoPeerId = MultiAddress.init("/ip4/127.0.0.1/tcp/80/ws").get()
addr2 = MultiAddress.init("/ip4/127.0.0.1/tcp/443/wss").get()
## When
var builder = EnrBuilder.init(enrPrivKey, seqNum = enrSeqNum)
builder.withMultiaddrs(@[addr1, addr1NoPeerId, addr2, addr2])
let recordRes = builder.build()
require recordRes.isOk()
let record = recordRes.tryGet()
let typedRecord = record.toTyped()
require typedRecord.isOk()
let multiaddrsOpt = typedRecord.value.multiaddrs
## Then
check multiaddrsOpt.isSome()
let multiaddrs = multiaddrsOpt.get()
check:
multiaddrs.len == 2
multiaddrs.contains(addr1NoPeerId)
multiaddrs.contains(addr2)
suite "Waku ENR - Relay static sharding":
test "new relay shards object with single invalid shard id":
## Given

View File

@ -5,7 +5,7 @@ import chronos, confutils/toml/std/net, libp2p/multiaddress, testutils/unittests
import ./testlib/wakunode, waku/waku_enr/capabilities
include
waku/node/net_config,
waku/net/net_config,
waku/factory/conf_builder/web_socket_conf_builder,
waku/factory/conf_builder/conf_builder
@ -152,6 +152,31 @@ suite "Waku NetConfig":
netConfig.announcedAddresses.len == 1 # DNS address
netConfig.announcedAddresses[0] == dns4TcpEndPoint(dns4DomainName, extPort)
asyncTest "AnnouncedAddresses and enrMultiaddrs deduplicate dns4DomainName and extMultiAddrs overlap":
let
conf = defaultTestWakuConf()
dns4DomainName = "example.com"
extPort = Port(1234)
dns4Address = dns4TcpEndPoint(dns4DomainName, extPort)
let netConfigRes = NetConfig.init(
bindIp = conf.endpointConf.p2pListenAddress,
bindPort = conf.endpointConf.p2pTcpPort,
dns4DomainName = some(dns4DomainName),
extPort = some(extPort),
extMultiAddrs = @[dns4Address],
)
assert netConfigRes.isOk(), $netConfigRes.error
let netConfig = netConfigRes.get()
check:
netConfig.announcedAddresses.len == 1
netConfig.announcedAddresses[0] == dns4Address
netConfig.enrMultiAddrs.len == 1
netConfig.enrMultiAddrs[0] == dns4Address
asyncTest "AnnouncedAddresses includes WebSocket addresses when enabled":
var confBuilder = defaultTestWakuConfBuilder()

View File

@ -27,7 +27,6 @@ import
# TODO: migrate to usage of a test cluster conf
proc defaultTestWakuConfBuilder*(): WakuConfBuilder =
var builder = WakuConfBuilder.init()
builder.withP2pTcpPort(Port(0))
builder.withP2pListenAddress(parseIpAddress("0.0.0.0"))
builder.restServerConf.withListenAddress(parseIpAddress("127.0.0.1"))
builder.withDnsAddrsNameServers(

View File

@ -506,7 +506,8 @@ suite "Waku Discovery v5":
waku.conf.nodeKey,
waku.conf.endpointConf.p2pListenAddress,
waku.conf.portsShift,
)
).valueOr:
raiseAssert "failed setup discv5 in test: " & $error
check:
waku.node.peerManager.switch.peerStore.peers().anyIt(
@ -537,7 +538,8 @@ suite "Waku Discovery v5":
waku.conf.nodeKey,
waku.conf.endpointConf.p2pListenAddress,
waku.conf.portsShift,
)
).valueOr:
raiseAssert "failed setup discv5 in test: " & $error
check:
not waku.node.peerManager.switch.peerStore.peers().anyIt(

View File

@ -1,6 +1,9 @@
{.used.}
import std/options, chronos, libp2p/crypto/crypto
import std/options, chronos, chronicles, libp2p/crypto/crypto
logScope:
topics = "test waku_lightpush_legacy"
import
waku/node/peer_manager,

View File

@ -8,17 +8,13 @@ import
libp2p/switch,
libp2p/protocols/pubsub/pubsub
import brokers/broker_context
from std/times import epochTime
import
waku/[
waku_relay,
node/waku_node,
node/peer_manager,
waku_core,
waku_node,
waku_rln_relay,
common/broker/broker_context,
waku_relay, node/waku_node, node/peer_manager, waku_core, waku_node, waku_rln_relay
],
../waku_store/store_utils,
../waku_archive/archive_utils,

View File

@ -1,11 +1,4 @@
import waku/waku_rln_relay/rln/rln_interface
proc `==`*(a: Buffer, b: seq[uint8]): bool =
if a.len != uint(b.len):
return false
let bufferArray = cast[ptr UncheckedArray[uint8]](a.ptr)
for i in 0 ..< b.len:
if bufferArray[i] != b[i]:
return false
return true
# buffer_utils.nim — intentionally empty.
# The v0.9 Buffer type and toBuffer helper were removed in the zerokit v2.0.1
# migration. This file is kept as a placeholder so that any future test imports
# do not break the build; the content that was here is no longer needed.

View File

@ -1,17 +1,36 @@
import testutils/unittests
import testutils/unittests, results
import waku/waku_rln_relay/rln/rln_interface, ./buffer_utils
import waku/waku_rln_relay/rln/rln_interface
import waku/waku_rln_relay/rln/wrappers
suite "Buffer":
suite "toBuffer":
suite "Vec_uint8":
suite "toVecUint8":
test "valid":
# Given
let bytes: seq[byte] = @[0x01, 0x02, 0x03]
# When
let buffer = bytes.toBuffer()
# When — wrap as a Vec_uint8 view then read the bytes back
var vec = toVecUint8(bytes)
let roundtrip = vecToSeq(vec)
# Then
let expectedBuffer: seq[uint8] = @[1, 2, 3]
# Then — byte values are preserved
check:
buffer == expectedBuffer
roundtrip == bytes
suite "RlnConfig":
suite "createRLNInstance":
test "ok":
# When we create the RLN instance (stateless build — no tree_depth arg)
let rlnRes = createRLNInstance()
# Then it succeeds
check:
rlnRes.isOk()
test "default":
# When we create the RLN instance
let rlnRes = createRLNInstance()
# Then it succeeds
check:
rlnRes.isOk()

View File

@ -1,37 +1,6 @@
import
std/options,
testutils/unittests,
chronicles,
chronos,
eth/keys,
bearssl,
stew/[results],
metrics,
metrics/chronos_httpserver
import testutils/unittests, results
import
waku/waku_rln_relay,
waku/waku_rln_relay/rln,
waku/waku_rln_relay/rln/wrappers,
./waku_rln_relay_utils,
../../testlib/[simple_mock, assertions],
../../waku_keystore/utils,
../../testlib/testutils
from std/times import epochTime
const Empty32Array = default(array[32, byte])
proc valid(x: seq[byte]): bool =
if x.len != 32:
error "Length should be 32", length = x.len
return false
if x == Empty32Array:
error "Should not be empty array", array = x
return false
return true
import waku/waku_rln_relay/rln, waku/waku_rln_relay/rln/wrappers, ./waku_rln_relay_utils
suite "membershipKeyGen":
test "ok":
@ -41,60 +10,20 @@ suite "membershipKeyGen":
# Then it contains valid identity credentials
let identityCredentials = identityCredentialsRes.get()
proc nonEmpty(x: seq[byte]): bool =
x.len == 32 and x != newSeq[byte](32)
check:
identityCredentials.idTrapdoor.valid()
identityCredentials.idNullifier.valid()
identityCredentials.idSecretHash.valid()
identityCredentials.idCommitment.valid()
test "done is false":
# Given the key_gen function fails
let backup = key_gen
mock(key_gen):
proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool =
return false
keyGenMock
# When we generate the membership keys
let identityCredentialsRes = membershipKeyGen()
# Then it fails
check:
identityCredentialsRes.error() == "error in key generation"
# Cleanup
mock(key_gen):
backup
test "generatedKeys length is not 128":
# Given the key_gen function succeeds with wrong values
let backup = key_gen
mock(key_gen):
proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool =
echo "# RUNNING MOCK"
output_buffer.len = 0
output_buffer.ptr = cast[ptr uint8](newSeq[byte](0))
return true
keyGenMock
# When we generate the membership keys
let identityCredentialsRes = membershipKeyGen()
# Then it fails
check:
identityCredentialsRes.error() == "keysBuffer is of invalid length"
# Cleanup
mock(key_gen):
backup
identityCredentials.idTrapdoor.nonEmpty()
identityCredentials.idNullifier.nonEmpty()
identityCredentials.idSecretHash.nonEmpty()
identityCredentials.idCommitment.nonEmpty()
suite "RlnConfig":
suite "createRLNInstance":
test "ok":
# When we create the RLN instance
let rlnRes: RLNResult = createRLNInstance(15)
# When we create the RLN instance (stateless build — no tree_depth arg)
let rlnRes = createRLNInstance()
# Then it succeeds
check:
@ -102,30 +31,8 @@ suite "RlnConfig":
test "default":
# When we create the RLN instance
let rlnRes: RLNResult = createRLNInstance()
let rlnRes = createRLNInstance()
# Then it succeeds
check:
rlnRes.isOk()
test "new_circuit fails":
# Given the new_circuit function fails
let backup = new_circuit
mock(new_circuit):
proc newCircuitMock(
tree_height: uint, input_buffer: ptr Buffer, ctx: ptr (ptr RLN)
): bool =
return false
newCircuitMock
# When we create the RLN instance
let rlnRes: RLNResult = createRLNInstance(15)
# Then it fails
check:
rlnRes.error() == "error in parameters generation"
# Cleanup
mock(new_circuit):
backup

View File

@ -3,7 +3,7 @@
{.push raises: [].}
import
std/[options, sequtils, deques, random, locks, osproc],
std/[options, sequtils, deques, random, locks, osproc, algorithm],
results,
stew/byteutils,
testutils/unittests,
@ -253,6 +253,9 @@ suite "Onchain group manager":
manager.merkleProofCache = newSeq[byte](640)
for i in 0 ..< 640:
manager.merkleProofCache[i] = byte(rand(255))
# chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30
for i in 0 ..< 20:
manager.merkleProofCache[i * 32] = 0
let messageBytes = "Hello".toBytes()
@ -335,6 +338,9 @@ suite "Onchain group manager":
manager.merkleProofCache = newSeq[byte](640)
for i in 0 ..< 640:
manager.merkleProofCache[i] = byte(rand(255))
# chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30
for i in 0 ..< 20:
manager.merkleProofCache[i * 32] = 0
let epoch = default(Epoch)
info "epoch in bytes", epochHex = epoch.inHex()
@ -419,3 +425,81 @@ suite "Onchain group manager":
check:
isReady == true
test "proof roundtrip: generateRlnProofWithWitness -> verifyRlnProof":
## Smoke test: proof gen -> wire serialize -> deserialize -> ffi_verify_with_roots.
let credentials = generateCredentials()
(waitFor manager.init()).isOkOr:
raiseAssert $error
(waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr:
assert false, "register failed: " & error
discard waitFor manager.updateRoots()
let roots = manager.validRoots.items().toSeq()
require:
roots.len > 0
let proofElements = (waitFor manager.fetchMerkleProofElements()).valueOr:
raiseAssert "fetchMerkleProofElements failed: " & error
let signal = "Hello, RLN!".toBytes()
let epoch = default(Epoch)
# Build RLNWitnessInput the same way group_manager.generateProof does.
var pathElements = newSeq[byte]()
for i in 0 ..< proofElements.len div 32:
pathElements.add(proofElements[i * 32 .. (i + 1) * 32 - 1].reversed())
let xCfr = hashToFieldLe(signal).valueOr:
raiseAssert "hashToFieldLe failed: " & error
defer:
ffi_cfr_free(xCfr)
let x = cfrToBytesLe(xCfr).valueOr:
raiseAssert "cfrToBytesLe failed: " & error
let extNullifier = generateExternalNullifier(epoch, DefaultRlnIdentifier).valueOr:
raiseAssert "generateExternalNullifier failed: " & error
let witness = RLNWitnessInput(
identity_secret: seqToField(credentials.idSecretHash),
user_message_limit: uint64ToField(uint64(UserMessageLimit(20))),
message_id: uint64ToField(uint64(MessageId(1))),
path_elements: pathElements,
identity_path_index: uint64ToIndex(manager.membershipIndex.get(), 20),
x: x,
external_nullifier: extNullifier,
)
# Step 1: generate proof via the FFI wrapper
let proof = generateRlnProofWithWitness(
manager.rlnInstance, witness, epoch, DefaultRlnIdentifier
).valueOr:
raiseAssert "generateRlnProofWithWitness failed: " & error
let zeroField = default(array[32, byte])
check:
proof.merkleRoot != zeroField
proof.nullifier != zeroField
# Step 2: serialize -> deserialize -> verify (the actual roundtrip)
let verified = verifyRlnProof(manager.rlnInstance, proof, signal, roots).valueOr:
raiseAssert "verifyRlnProof failed: " & error
check verified == true
# Step 3: wrong signal -> x mismatch -> false
let wrongSignalVerified = verifyRlnProof(
manager.rlnInstance, proof, "wrong".toBytes(), roots
).valueOr:
raiseAssert "verifyRlnProof (wrong signal) failed: " & error
check wrongSignalVerified == false
# Step 4: bad root -> root not in set -> false
# byte[31] in LE is the MSB; 0x01 < 0x30 so this is a canonical field element.
var badRoot: MerkleNode
for i in 0 ..< 32:
badRoot[i] = 0x01
let badRootVerified = verifyRlnProof(manager.rlnInstance, proof, signal, @[badRoot]).valueOr:
raiseAssert "verifyRlnProof (bad root) failed: " & error
check badRootVerified == false

View File

@ -1,13 +1,16 @@
{.used.}
import
std/[options, os, sequtils, tempfiles, strutils, osproc],
std/[options, os, sequtils, tempfiles, strutils, osproc, algorithm],
stew/byteutils,
testutils/unittests,
chronos,
chronicles,
stint,
libp2p/crypto/crypto
import brokers/broker_context
import
waku/[
waku_core,
@ -15,7 +18,6 @@ import
waku_rln_relay/rln,
waku_rln_relay/protocol_metrics,
waku_keystore,
common/broker/broker_context,
],
./rln/waku_rln_relay_utils,
./utils_onchain,
@ -34,23 +36,16 @@ suite "Waku rln relay":
teardown:
stopAnvil(anvilProc)
test "key_gen Nim Wrappers":
let merkleDepth: csize_t = 20
test "ffi_extended_key_gen raw FFI":
# When we call the raw key-generation FFI
var vec = ffi_extended_key_gen()
# keysBufferPtr will hold the generated identity credential i.e., id trapdoor, nullifier, secret hash and commitment
var keysBuffer: Buffer
let
keysBufferPtr = addr(keysBuffer)
done = key_gen(keysBufferPtr, true)
require:
# check whether the keys are generated successfully
done
let generatedKeys = cast[ptr array[4 * 32, byte]](keysBufferPtr.`ptr`)[]
# Then it returns exactly 4 field elements
# (idTrapdoor, idNullifier, idSecretHash, idCommitment — each 32 bytes)
defer:
ffi_vec_cfr_free(vec)
check:
# the id trapdoor, nullifier, secert hash and commitment together are 4*32 bytes
generatedKeys.len == 4 * 32
info "generated keys: ", generatedKeys
int(ffi_vec_cfr_len(addr vec)) == 4
test "membership Key Generation":
let idCredentialsRes = membershipKeyGen()
@ -78,18 +73,22 @@ suite "Waku rln relay":
rlnInstance.isOk()
let rln = rlnInstance.get()
# prepare the input
let msg = @[
"126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc".toBytes(),
"1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1".toBytes(),
]
# prepare the input — hex-decoded then reversed to little-endian field elements
let
left = hexToSeqByte(
"126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc"
)
.reversed()
right = hexToSeqByte(
"1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1"
)
.reversed()
let hashRes = poseidon(msg)
let hashRes = poseidon(left, right)
# Value taken from zerokit
check:
hashRes.isOk()
"28a15a991fe3d2a014485c7fa905074bfb55c0909112f865ded2be0a26a932c3" ==
"180543bc9afb81d9c2282df9c9946f87b4596cf6d3fec2cc32b6637427685353" ==
hashRes.get().inHex()
test "RateLimitProof Protobuf encode/init test":

View File

@ -7,13 +7,14 @@ import
chronicles,
chronos,
libp2p/switch,
libp2p/protocols/pubsub/pubsub
libp2p/protocols/pubsub/pubsub,
brokers/broker_context
import
waku/[waku_core, waku_node, waku_rln_relay],
../testlib/[wakucore, futures, wakunode, testutils],
./utils_onchain,
./rln/waku_rln_relay_utils,
waku/common/broker/broker_context
./rln/waku_rln_relay_utils
from std/times import epochTime

View File

@ -1,6 +1,6 @@
{.used.}
import std/options, testutils/unittests, chronos, libp2p/crypto/crypto
import std/[options, sets], testutils/unittests, chronos, libp2p/crypto/crypto
import
waku/[node/peer_manager, waku_core, waku_store, waku_store/client, common/paging],
@ -223,3 +223,24 @@ suite "Store Client":
not await handlerFuture.withTimeout(FUTURE_TIMEOUT)
queryResponse.isErr()
queryResponse.error.kind == ErrorCode.PEER_DIAL_FAILURE
asyncTest "queryToAny shuffles peers across calls":
# Register several fake store peers (no servers running) so every dial
# fails. PEER_DIAL_FAILURE carries the peerId of the last peer tried in
# the shuffled order, so observing different "last" peerIds across calls
# confirms shuffle is active inside queryToAny.
for _ in 0 ..< 3:
let fakeSwitch = newTestSwitch()
let peerInfo = fakeSwitch.peerInfo.toRemotePeerInfo()
peerInfo.protocols = @[WakuStoreCodec]
clientSwitch.peerStore.addPeer(peerInfo)
var observedLastPeers: HashSet[string]
for _ in 0 ..< 20:
let res = await client.queryToAny(storeQuery)
check:
res.isErr()
res.error.kind == ErrorCode.PEER_DIAL_FAILURE
observedLastPeers.incl(res.error.address)
check observedLastPeers.len >= 2

View File

@ -1,14 +1,13 @@
{.used.}
import
std/json,
testutils/unittests,
chronicles,
chronos,
libp2p/crypto/crypto,
libp2p/crypto/secp,
libp2p/multiaddress,
libp2p/switch
import ../testlib/wakucore, ../testlib/wakunode
libp2p/[crypto/crypto, crypto/secp, multiaddress, switch],
tests/testlib/[wakucore, wakunode],
waku/factory/conf_builder/conf_builder
include waku/factory/waku, waku/common/enr/typed_record
@ -99,3 +98,46 @@ suite "Wakunode2 - Waku initialization":
## Cleanup
(waitFor waku.stop()).isOkOr:
raiseAssert error
test "explicit port=0 triggers auto-bind across all services":
var builder = defaultTestWakuConfBuilder()
builder.withP2pTcpPort(Port(0))
builder.discv5Conf.withEnabled(true)
builder.discv5Conf.withUdpPort(Port(0))
builder.restServerConf.withEnabled(true)
builder.restServerConf.withRelayCacheCapacity(50'u32)
builder.restServerConf.withPort(Port(0))
builder.metricsServerConf.withEnabled(true)
builder.metricsServerConf.withHttpPort(Port(0))
builder.webSocketConf.withEnabled(true)
builder.webSocketConf.withWebSocketPort(Port(0))
let conf = builder.build().valueOr:
raiseAssert error
check:
conf.endpointConf.p2pTcpPort == Port(0)
conf.discv5Conf.get().udpPort == Port(0)
conf.restServerConf.get().port == Port(0)
conf.metricsServerConf.get().httpPort == Port(0)
conf.webSocketConf.get().port == Port(0)
var waku = (waitFor Waku.new(conf)).valueOr:
raiseAssert error
defer:
(waitFor waku.stop()).isOkOr:
raiseAssert error
(waitFor startWaku(addr waku)).isOkOr:
raiseAssert error
let portsJson = waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts)
let parsed = parseJson(portsJson)
check:
parsed.kind == JObject
parsed["tcp"].getInt() != 0
parsed["webSocket"].getInt() != 0
parsed["rest"].getInt() != 0
parsed["discv5Udp"].getInt() != 0
parsed["metrics"].getInt() != 0

View File

@ -1,6 +1,7 @@
{.used.}
import
std/options,
testutils/unittests,
presto,
presto/client as presto_client,

View File

@ -7,6 +7,7 @@ import
presto,
presto/client as presto_client,
libp2p/crypto/crypto
import brokers/broker_context
import
waku/[
common/base64,
@ -21,7 +22,6 @@ import
rest_api/endpoint/relay/client as relay_rest_client,
waku_relay,
waku_rln_relay,
common/broker/broker_context,
],
../testlib/wakucore,
../testlib/wakunode,

View File

@ -698,6 +698,12 @@ with the drawback of consuming some more bandwidth.""",
name: "rate-limit"
.}: seq[string]
localStoragePath* {.
desc: "Path to store local data.",
defaultValue: "./data",
name: "local-storage-path"
.}: string
## Parsing
# NOTE: Keys are different in nim-libp2p
@ -1119,6 +1125,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
if n.rateLimits.len > 0:
b.rateLimitConf.withRateLimits(n.rateLimits)
b.withLocalStoragePath(n.localStoragePath)
b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery)
b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes)

View File

@ -36,7 +36,10 @@ fi
echo "[*] Generating $OUTFILE from $LOCKFILE"
mkdir -p "$(dirname "$OUTFILE")"
cat > "$OUTFILE" <<'EOF'
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
cat > "$TMPFILE" <<'EOF'
# AUTOGENERATED from nimble.lock — do not edit manually.
# Regenerate with: ./tools/gen-nix-deps.sh nimble.lock nix/deps.nix
{ pkgs }:
@ -62,7 +65,7 @@ jq -c '
--fetch-submodules \
| jq -r '.sha256')
cat >> "$OUTFILE" <<EOF
cat >> "$TMPFILE" <<EOF
${name} = pkgs.fetchgit {
url = "${url}";
rev = "${rev}";
@ -73,8 +76,9 @@ jq -c '
EOF
done
cat >> "$OUTFILE" <<'EOF'
cat >> "$TMPFILE" <<'EOF'
}
EOF
mv "$TMPFILE" "$OUTFILE"
echo "[✓] Wrote $OUTFILE"

2
vendor/zerokit vendored

@ -1 +1 @@
Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b
Subproject commit 5e64cb8822bee65eed6cf459f95ae72b80c6ba63

View File

@ -4,7 +4,7 @@ import os
mode = ScriptMode.Verbose
### Package
version = "0.37.4"
version = "0.38.1"
author = "Status Research & Development GmbH"
description = "Waku, Private P2P Messaging for Resource-Restricted Devices"
license = "MIT or Apache License 2.0"
@ -33,7 +33,7 @@ requires "nim >= 2.2.4",
"dnsdisc",
"dnsclient",
"httputils >= 0.4.1",
"websock >= 0.2.1",
"websock >= 0.3.0",
# Cryptography
"nimcrypto == 0.6.4", # 0.6.4 used in libp2p. Version 0.7.3 makes test to crash on Ubuntu.
"secp256k1",
@ -61,6 +61,18 @@ requires "nim >= 2.2.4",
# Packages not on nimble (use git URLs)
requires "https://github.com/logos-messaging/nim-ffi"
requires "https://github.com/logos-messaging/nim-sds.git#2e9a7683f0e180bf112135fae3a3803eed8490d4"
# brokers: pinned by URL+commit rather than the bare `brokers >= 2.0.1`
# form because the nim-lang/packages registry entry for `brokers` only
# carries metadata for the original v0.1.0 publication. Until that
# registry entry is refreshed, the local SAT solver enumerates "0.1.0"
# as the only available version and cannot satisfy `>= 2.0.1`. The URL
# pin below bypasses the registry and locks the exact commit of the
# v2.0.1 tag. Revert to the bare form once nim-lang/packages is
# updated.
requires "https://github.com/NagyZoltanPeter/nim-brokers.git#v2.0.1"
requires "https://github.com/vacp2p/nim-lsquic"
requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2"

View File

@ -1,68 +0,0 @@
{.push raises: [].}
import std/[strutils, concurrency/atomics], chronos
type BrokerContext* = distinct uint32
func `==`*(a, b: BrokerContext): bool =
uint32(a) == uint32(b)
func `!=`*(a, b: BrokerContext): bool =
uint32(a) != uint32(b)
func `$`*(bc: BrokerContext): string =
toHex(uint32(bc), 8)
const DefaultBrokerContext* = BrokerContext(0xCAFFE14E'u32)
# Global broker context accessor.
#
# NOTE: This intentionally creates a *single* active BrokerContext per process
# (per event loop thread). Use only if you accept serialization of all broker
# context usage through the lock.
var globalBrokerContextLock {.threadvar.}: AsyncLock
globalBrokerContextLock = newAsyncLock()
var globalBrokerContextValue {.threadvar.}: BrokerContext
globalBrokerContextValue = DefaultBrokerContext
proc globalBrokerContext*(): BrokerContext =
## Returns the currently active global broker context.
##
## This is intentionally lock-free; callers should use it inside
## `withNewGlobalBrokerContext` / `withGlobalBrokerContext`.
globalBrokerContextValue
var gContextCounter: Atomic[uint32]
proc NewBrokerContext*(): BrokerContext =
var nextId = gContextCounter.fetchAdd(1, moRelaxed)
if nextId == uint32(DefaultBrokerContext):
nextId = gContextCounter.fetchAdd(1, moRelaxed)
return BrokerContext(nextId)
template lockGlobalBrokerContext*(brokerCtx: BrokerContext, body: untyped): untyped =
## Runs `body` while holding the global broker context lock with the provided
## `brokerCtx` installed as the globally accessible context.
##
## This template is intended for use from within `chronos` async procs.
block:
await noCancel(globalBrokerContextLock.acquire())
let previousBrokerCtx = globalBrokerContextValue
globalBrokerContextValue = brokerCtx
try:
body
finally:
globalBrokerContextValue = previousBrokerCtx
try:
globalBrokerContextLock.release()
except AsyncLockError:
doAssert false, "globalBrokerContextLock.release(): lock not held"
template lockNewGlobalBrokerContext*(body: untyped): untyped =
## Runs `body` while holding the global broker context lock with a freshly
## generated broker context installed as the global accessor.
##
## The previous global broker context (if any) is restored on exit.
lockGlobalBrokerContext(NewBrokerContext()):
body
{.pop.}

View File

@ -1,411 +0,0 @@
## EventBroker
## -------------------
## EventBroker represents a reactive decoupling pattern, that
## allows event-driven development without
## need for direct dependencies in between emitters and listeners.
## Worth considering using it in a single or many emitters to many listeners scenario.
##
## Generates a standalone, type-safe event broker for the declared type.
## The macro exports the value type itself plus a broker companion that manages
## listeners via thread-local storage.
##
## Type definitions:
## - Inline `object` / `ref object` definitions are supported.
## - Native types, aliases, and externally-defined types are also supported.
## In that case, EventBroker will automatically wrap the declared RHS type in
## `distinct` unless you already used `distinct`.
## This keeps event types unique even when multiple brokers share the same
## underlying base type.
##
## Default vs. context aware use:
## Every generated broker is a thread-local global instance. This means EventBroker
## enables decoupled event exchange threadwise.
##
## Sometimes we use brokers inside a context (e.g. within a component that has many
## modules or subsystems). If you instantiate multiple such components in a single
## thread, and each component must have its own listener set for the same EventBroker
## type, you can use context-aware EventBroker.
##
## Context awareness is supported through the `BrokerContext` argument for
## `listen`, `emit`, `dropListener`, and `dropAllListeners`.
## Listener stores are kept separate per broker context.
##
## Default broker context is defined as `DefaultBrokerContext`. If you don't need
## context awareness, you can keep using the interfaces without the context
## argument, which operate on `DefaultBrokerContext`.
##
## Usage:
## Declare your desired event type inside an `EventBroker` macro, add any number of fields.:
## ```nim
## EventBroker:
## type TypeName = object
## field1*: FieldType
## field2*: AnotherFieldType
## ```
##
## After this, you can register async listeners anywhere in your code with
## `TypeName.listen(...)`, which returns a handle to the registered listener.
## Listeners are async procs or lambdas that take a single argument of the event type.
## Any number of listeners can be registered in different modules.
##
## Events can be emitted from anywhere with no direct dependency on the listeners by
## calling `TypeName.emit(...)` with an instance of the event type.
## This will asynchronously notify all registered listeners with the emitted event.
##
## Whenever you no longer need a listener (or your object instance that listen to the event goes out of scope),
## you can remove it from the broker with the handle returned by `listen`.
## This is done by calling `TypeName.dropListener(handle)`.
## Alternatively, you can remove all registered listeners through `TypeName.dropAllListeners()`.
##
##
## Example:
## ```nim
## EventBroker:
## type GreetingEvent = object
## text*: string
##
## let handle = GreetingEvent.listen(
## proc(evt: GreetingEvent): Future[void] {.async.} =
## echo evt.text
## )
## GreetingEvent.emit(text= "hi")
## GreetingEvent.dropListener(handle)
## ```
## Example (non-object event type):
## ```nim
## EventBroker:
## type CounterEvent = int # exported as: `distinct int`
##
## discard CounterEvent.listen(
## proc(evt: CounterEvent): Future[void] {.async.} =
## echo int(evt)
## )
## CounterEvent.emit(CounterEvent(42))
## ```
import std/[macros, tables]
import chronos, chronicles, results
import ./helper/broker_utils, broker_context
export chronicles, results, chronos, broker_context
macro EventBroker*(body: untyped): untyped =
when defined(eventBrokerDebug):
echo body.treeRepr
let parsed = parseSingleTypeDef(body, "EventBroker", collectFieldInfo = true)
let typeIdent = parsed.typeIdent
let objectDef = parsed.objectDef
let fieldNames = parsed.fieldNames
let fieldTypes = parsed.fieldTypes
let hasInlineFields = parsed.hasInlineFields
let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*")
let sanitized = sanitizeIdentName(typeIdent)
let typeNameLit = newLit($typeIdent)
let handlerProcIdent = ident(sanitized & "ListenerProc")
let listenerHandleIdent = ident(sanitized & "Listener")
let brokerTypeIdent = ident(sanitized & "Broker")
let exportedHandlerProcIdent = postfix(copyNimTree(handlerProcIdent), "*")
let exportedListenerHandleIdent = postfix(copyNimTree(listenerHandleIdent), "*")
let exportedBrokerTypeIdent = postfix(copyNimTree(brokerTypeIdent), "*")
let bucketTypeIdent = ident(sanitized & "CtxBucket")
let findBucketIdxIdent = ident(sanitized & "FindBucketIdx")
let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx")
let accessProcIdent = ident("access" & sanitized & "Broker")
let globalVarIdent = ident("g" & sanitized & "Broker")
let listenImplIdent = ident("register" & sanitized & "Listener")
let dropListenerImplIdent = ident("drop" & sanitized & "Listener")
let dropAllListenersImplIdent = ident("dropAll" & sanitized & "Listeners")
let emitImplIdent = ident("emit" & sanitized & "Value")
let listenerTaskIdent = ident("notify" & sanitized & "Listener")
result = newStmtList()
result.add(
quote do:
type
`exportedTypeIdent` = `objectDef`
`exportedListenerHandleIdent` = object
id*: uint64
`exportedHandlerProcIdent` =
proc(event: `typeIdent`): Future[void] {.async: (raises: []), gcsafe.}
`bucketTypeIdent` = object
brokerCtx: BrokerContext
listeners: Table[uint64, `handlerProcIdent`]
nextId: uint64
`exportedBrokerTypeIdent` = ref object
buckets: seq[`bucketTypeIdent`]
)
result.add(
quote do:
var `globalVarIdent` {.threadvar.}: `brokerTypeIdent`
)
result.add(
quote do:
proc `accessProcIdent`(): `brokerTypeIdent` =
if `globalVarIdent`.isNil():
new(`globalVarIdent`)
`globalVarIdent`.buckets = @[
`bucketTypeIdent`(
brokerCtx: DefaultBrokerContext,
listeners: initTable[uint64, `handlerProcIdent`](),
nextId: 1'u64,
)
]
`globalVarIdent`
)
result.add(
quote do:
proc `findBucketIdxIdent`(
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
): int =
if brokerCtx == DefaultBrokerContext:
return 0
for i in 1 ..< broker.buckets.len:
if broker.buckets[i].brokerCtx == brokerCtx:
return i
return -1
proc `getOrCreateBucketIdxIdent`(
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
): int =
let idx = `findBucketIdxIdent`(broker, brokerCtx)
if idx >= 0:
return idx
broker.buckets.add(
`bucketTypeIdent`(
brokerCtx: brokerCtx,
listeners: initTable[uint64, `handlerProcIdent`](),
nextId: 1'u64,
)
)
return broker.buckets.high
proc `listenImplIdent`(
brokerCtx: BrokerContext, handler: `handlerProcIdent`
): Result[`listenerHandleIdent`, string] =
if handler.isNil():
return err("Must provide a non-nil event handler")
var broker = `accessProcIdent`()
let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx)
if broker.buckets[bucketIdx].nextId == 0'u64:
broker.buckets[bucketIdx].nextId = 1'u64
if broker.buckets[bucketIdx].nextId == high(uint64):
error "Cannot add more listeners: ID space exhausted",
nextId = $broker.buckets[bucketIdx].nextId
return err("Cannot add more listeners, listener ID space exhausted")
let newId = broker.buckets[bucketIdx].nextId
inc broker.buckets[bucketIdx].nextId
broker.buckets[bucketIdx].listeners[newId] = handler
return ok(`listenerHandleIdent`(id: newId))
)
result.add(
quote do:
proc `dropListenerImplIdent`(
brokerCtx: BrokerContext, handle: `listenerHandleIdent`
) =
if handle.id == 0'u64:
return
var broker = `accessProcIdent`()
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
if broker.buckets[bucketIdx].listeners.len == 0:
return
broker.buckets[bucketIdx].listeners.del(handle.id)
if brokerCtx != DefaultBrokerContext and
broker.buckets[bucketIdx].listeners.len == 0:
broker.buckets.delete(bucketIdx)
)
result.add(
quote do:
proc `dropAllListenersImplIdent`(brokerCtx: BrokerContext) =
var broker = `accessProcIdent`()
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
if broker.buckets[bucketIdx].listeners.len > 0:
broker.buckets[bucketIdx].listeners.clear()
if brokerCtx != DefaultBrokerContext:
broker.buckets.delete(bucketIdx)
)
result.add(
quote do:
proc listen*(
_: typedesc[`typeIdent`], handler: `handlerProcIdent`
): Result[`listenerHandleIdent`, string] =
return `listenImplIdent`(DefaultBrokerContext, handler)
proc listen*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
handler: `handlerProcIdent`,
): Result[`listenerHandleIdent`, string] =
return `listenImplIdent`(brokerCtx, handler)
)
result.add(
quote do:
proc dropListener*(_: typedesc[`typeIdent`], handle: `listenerHandleIdent`) =
`dropListenerImplIdent`(DefaultBrokerContext, handle)
proc dropListener*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
handle: `listenerHandleIdent`,
) =
`dropListenerImplIdent`(brokerCtx, handle)
proc dropAllListeners*(_: typedesc[`typeIdent`]) =
`dropAllListenersImplIdent`(DefaultBrokerContext)
proc dropAllListeners*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
`dropAllListenersImplIdent`(brokerCtx)
)
result.add(
quote do:
proc `listenerTaskIdent`(
callback: `handlerProcIdent`, event: `typeIdent`
) {.async: (raises: []), gcsafe.} =
if callback.isNil():
return
try:
await callback(event)
except Exception:
error "Failed to execute event listener", error = getCurrentExceptionMsg()
proc `emitImplIdent`(
brokerCtx: BrokerContext, event: `typeIdent`
): Future[void] {.async: (raises: []), gcsafe.} =
when compiles(event.isNil()):
if event.isNil():
error "Cannot emit uninitialized event object", eventType = `typeNameLit`
return
let broker = `accessProcIdent`()
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
# nothing to do as nobody is listening
return
if broker.buckets[bucketIdx].listeners.len == 0:
return
var callbacks: seq[`handlerProcIdent`] = @[]
for cb in broker.buckets[bucketIdx].listeners.values:
callbacks.add(cb)
for cb in callbacks:
asyncSpawn `listenerTaskIdent`(cb, event)
proc emit*(event: `typeIdent`) =
asyncSpawn `emitImplIdent`(DefaultBrokerContext, event)
proc emit*(_: typedesc[`typeIdent`], event: `typeIdent`) =
asyncSpawn `emitImplIdent`(DefaultBrokerContext, event)
proc emit*(
_: typedesc[`typeIdent`], brokerCtx: BrokerContext, event: `typeIdent`
) =
asyncSpawn `emitImplIdent`(brokerCtx, event)
)
if hasInlineFields:
# Typedesc emit constructor overloads for inline object/ref object types.
var emitCtorParams = newTree(nnkFormalParams, newEmptyNode())
let typedescParamType =
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent))
emitCtorParams.add(
newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode())
)
for i in 0 ..< fieldNames.len:
emitCtorParams.add(
newTree(
nnkIdentDefs,
copyNimTree(fieldNames[i]),
copyNimTree(fieldTypes[i]),
newEmptyNode(),
)
)
var emitCtorExpr = newTree(nnkObjConstr, copyNimTree(typeIdent))
for i in 0 ..< fieldNames.len:
emitCtorExpr.add(
newTree(
nnkExprColonExpr, copyNimTree(fieldNames[i]), copyNimTree(fieldNames[i])
)
)
let emitCtorCallDefault =
newCall(copyNimTree(emitImplIdent), ident("DefaultBrokerContext"), emitCtorExpr)
let emitCtorBodyDefault = quote:
asyncSpawn `emitCtorCallDefault`
let typedescEmitProcDefault = newTree(
nnkProcDef,
postfix(ident("emit"), "*"),
newEmptyNode(),
newEmptyNode(),
emitCtorParams,
newEmptyNode(),
newEmptyNode(),
emitCtorBodyDefault,
)
result.add(typedescEmitProcDefault)
var emitCtorParamsCtx = newTree(nnkFormalParams, newEmptyNode())
emitCtorParamsCtx.add(
newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode())
)
emitCtorParamsCtx.add(
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
)
for i in 0 ..< fieldNames.len:
emitCtorParamsCtx.add(
newTree(
nnkIdentDefs,
copyNimTree(fieldNames[i]),
copyNimTree(fieldTypes[i]),
newEmptyNode(),
)
)
let emitCtorCallCtx =
newCall(copyNimTree(emitImplIdent), ident("brokerCtx"), copyNimTree(emitCtorExpr))
let emitCtorBodyCtx = quote:
asyncSpawn `emitCtorCallCtx`
let typedescEmitProcCtx = newTree(
nnkProcDef,
postfix(ident("emit"), "*"),
newEmptyNode(),
newEmptyNode(),
emitCtorParamsCtx,
newEmptyNode(),
newEmptyNode(),
emitCtorBodyCtx,
)
result.add(typedescEmitProcCtx)
when defined(eventBrokerDebug):
echo result.repr

View File

@ -1,206 +0,0 @@
import std/macros
type ParsedBrokerType* = object
## Result of parsing the single `type` definition inside a broker macro body.
##
## - `typeIdent`: base identifier for the declared type name
## - `objectDef`: exported type definition RHS (inline object fields exported;
## non-object types wrapped in `distinct` unless already distinct)
## - `isRefObject`: true only for inline `ref object` definitions
## - `hasInlineFields`: true for inline `object` / `ref object`
## - `fieldNames`/`fieldTypes`: populated only when `collectFieldInfo = true`
typeIdent*: NimNode
objectDef*: NimNode
isRefObject*: bool
hasInlineFields*: bool
fieldNames*: seq[NimNode]
fieldTypes*: seq[NimNode]
proc sanitizeIdentName*(node: NimNode): string =
var raw = $node
var sanitizedName = newStringOfCap(raw.len)
for ch in raw:
case ch
of 'A' .. 'Z', 'a' .. 'z', '0' .. '9', '_':
sanitizedName.add(ch)
else:
sanitizedName.add('_')
sanitizedName
proc ensureFieldDef*(node: NimNode) =
if node.kind != nnkIdentDefs or node.len < 3:
error("Expected field definition of the form `name: Type`", node)
let typeSlot = node.len - 2
if node[typeSlot].kind == nnkEmpty:
error("Field `" & $node[0] & "` must declare a type", node)
proc exportIdentNode*(node: NimNode): NimNode =
case node.kind
of nnkIdent:
postfix(copyNimTree(node), "*")
of nnkPostfix:
node
else:
error("Unsupported identifier form in field definition", node)
proc baseTypeIdent*(defName: NimNode): NimNode =
case defName.kind
of nnkIdent:
defName
of nnkAccQuoted:
if defName.len != 1:
error("Unsupported quoted identifier", defName)
defName[0]
of nnkPostfix:
baseTypeIdent(defName[1])
of nnkPragmaExpr:
baseTypeIdent(defName[0])
else:
error("Unsupported type name in broker definition", defName)
proc ensureDistinctType*(rhs: NimNode): NimNode =
## For PODs / aliases / externally-defined types, wrap in `distinct` unless
## it's already distinct.
if rhs.kind == nnkDistinctTy:
return copyNimTree(rhs)
newTree(nnkDistinctTy, copyNimTree(rhs))
proc cloneParams*(params: seq[NimNode]): seq[NimNode] =
## Deep copy parameter definitions so they can be inserted in multiple places.
result = @[]
for param in params:
result.add(copyNimTree(param))
proc collectParamNames*(params: seq[NimNode]): seq[NimNode] =
## Extract all identifier symbols declared across IdentDefs nodes.
result = @[]
for param in params:
assert param.kind == nnkIdentDefs
for i in 0 ..< param.len - 2:
let nameNode = param[i]
if nameNode.kind == nnkEmpty:
continue
result.add(ident($nameNode))
proc parseSingleTypeDef*(
body: NimNode,
macroName: string,
allowRefToNonObject = false,
collectFieldInfo = false,
): ParsedBrokerType =
## Parses exactly one `type` definition from a broker macro body.
##
## Supported RHS:
## - inline `object` / `ref object` (fields are auto-exported)
## - non-object types / aliases / externally-defined types (wrapped in `distinct`)
## - optionally: `ref SomeType` when `allowRefToNonObject = true`
var typeIdent: NimNode = nil
var objectDef: NimNode = nil
var isRefObject = false
var hasInlineFields = false
var fieldNames: seq[NimNode] = @[]
var fieldTypes: seq[NimNode] = @[]
for stmt in body:
if stmt.kind != nnkTypeSection:
continue
for def in stmt:
if def.kind != nnkTypeDef:
continue
if not typeIdent.isNil():
error("Only one type may be declared inside " & macroName, def)
typeIdent = baseTypeIdent(def[0])
let rhs = def[2]
case rhs.kind
of nnkObjectTy:
let recList = rhs[2]
if recList.kind != nnkRecList:
error(macroName & " object must declare a standard field list", rhs)
var exportedRecList = newTree(nnkRecList)
for field in recList:
case field.kind
of nnkIdentDefs:
ensureFieldDef(field)
if collectFieldInfo:
let fieldTypeNode = field[field.len - 2]
for i in 0 ..< field.len - 2:
let baseFieldIdent = baseTypeIdent(field[i])
fieldNames.add(copyNimTree(baseFieldIdent))
fieldTypes.add(copyNimTree(fieldTypeNode))
var cloned = copyNimTree(field)
for i in 0 ..< cloned.len - 2:
cloned[i] = exportIdentNode(cloned[i])
exportedRecList.add(cloned)
of nnkEmpty:
discard
else:
error(
macroName & " object definition only supports simple field declarations",
field,
)
objectDef = newTree(
nnkObjectTy, copyNimTree(rhs[0]), copyNimTree(rhs[1]), exportedRecList
)
isRefObject = false
hasInlineFields = true
of nnkRefTy:
if rhs.len != 1:
error(macroName & " ref type must have a single base", rhs)
if rhs[0].kind == nnkObjectTy:
let obj = rhs[0]
let recList = obj[2]
if recList.kind != nnkRecList:
error(macroName & " object must declare a standard field list", obj)
var exportedRecList = newTree(nnkRecList)
for field in recList:
case field.kind
of nnkIdentDefs:
ensureFieldDef(field)
if collectFieldInfo:
let fieldTypeNode = field[field.len - 2]
for i in 0 ..< field.len - 2:
let baseFieldIdent = baseTypeIdent(field[i])
fieldNames.add(copyNimTree(baseFieldIdent))
fieldTypes.add(copyNimTree(fieldTypeNode))
var cloned = copyNimTree(field)
for i in 0 ..< cloned.len - 2:
cloned[i] = exportIdentNode(cloned[i])
exportedRecList.add(cloned)
of nnkEmpty:
discard
else:
error(
macroName & " object definition only supports simple field declarations",
field,
)
let exportedObjectType = newTree(
nnkObjectTy, copyNimTree(obj[0]), copyNimTree(obj[1]), exportedRecList
)
objectDef = newTree(nnkRefTy, exportedObjectType)
isRefObject = true
hasInlineFields = true
elif allowRefToNonObject:
## `ref SomeType` (SomeType can be defined elsewhere)
objectDef = ensureDistinctType(rhs)
isRefObject = false
hasInlineFields = false
else:
error(macroName & " ref object must wrap a concrete object definition", rhs)
else:
## Non-object type / alias.
objectDef = ensureDistinctType(rhs)
isRefObject = false
hasInlineFields = false
if typeIdent.isNil():
error(macroName & " body must declare exactly one type", body)
result = ParsedBrokerType(
typeIdent: typeIdent,
objectDef: objectDef,
isRefObject: isRefObject,
hasInlineFields: hasInlineFields,
fieldNames: fieldNames,
fieldTypes: fieldTypes,
)

View File

@ -1,743 +0,0 @@
## MultiRequestBroker
## --------------------
## MultiRequestBroker represents a proactive decoupling pattern, that
## allows defining request-response style interactions between modules without
## need for direct dependencies in between.
## Worth considering using it for use cases where you need to collect data from multiple providers.
##
## Generates a standalone, type-safe request broker for the declared type.
## The macro exports the value type itself plus a broker companion that manages
## providers via thread-local storage.
##
## Unlike `RequestBroker`, every call to `request` fan-outs to every registered
## provider and returns all collected responses.
## The request succeeds only if all providers succeed, otherwise it fails.
##
## Type definitions:
## - Inline `object` / `ref object` definitions are supported.
## - Native types, aliases, and externally-defined types are also supported.
## In that case, MultiRequestBroker will automatically wrap the declared RHS
## type in `distinct` unless you already used `distinct`.
## This keeps request types unique even when multiple brokers share the same
## underlying base type.
##
## Default vs. context aware use:
## Every generated broker is a thread-local global instance.
## Sometimes you want multiple independent provider sets for the same request
## type within the same thread (e.g. multiple components). For that, you can use
## context-aware MultiRequestBroker.
##
## Context awareness is supported through the `BrokerContext` argument for
## `setProvider`, `request`, `removeProvider`, and `clearProviders`.
## Provider stores are kept separate per broker context.
##
## Default broker context is defined as `DefaultBrokerContext`. If you don't
## need context awareness, you can keep using the interfaces without the context
## argument, which operate on `DefaultBrokerContext`.
##
## Usage:
##
## Declare collectable request data type inside a `MultiRequestBroker` macro, add any number of fields:
## ```nim
## MultiRequestBroker:
## type TypeName = object
## field1*: Type1
## field2*: Type2
##
## ## Define the request and provider signature, that is enforced at compile time.
## proc signature*(): Future[Result[TypeName, string]] {.async: (raises: []).}
##
## ## Also possible to define signature with arbitrary input arguments.
## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]] {.async: (raises: []).}
##
## ```
##
## You can register a request processor (provider) anywhere without the need to
## know who will request.
## Register provider functions with `TypeName.setProvider(...)`.
## Providers are async procs or lambdas that return `Future[Result[TypeName, string]]`.
## `setProvider` returns a handle (or an error) that can later be used to remove
## the provider.
## Requests can be made from anywhere with no direct dependency on the provider(s)
## by calling `TypeName.request()` (with arguments respecting the declared signature).
## This will asynchronously call all registered providers and return the collected
## responses as `Future[Result[seq[TypeName], string]]`.
##
## Whenever you don't want to process requests anymore (or your object instance that provides the request goes out of scope),
## you can remove it from the broker with `TypeName.removeProvider(handle)`.
## Alternatively, you can remove all registered providers through `TypeName.clearProviders()`.
##
## Example:
## ```nim
## MultiRequestBroker:
## type Greeting = object
## text*: string
##
## ## Define the request and provider signature, that is enforced at compile time.
## proc signature*(): Future[Result[Greeting, string]] {.async: (raises: []).}
##
## ## Also possible to define signature with arbitrary input arguments.
## proc signature*(lang: string): Future[Result[Greeting, string]] {.async: (raises: []).}
##
## ...
## let handle = Greeting.setProvider(
## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} =
## ok(Greeting(text: "hello"))
## )
##
## let anotherHandle = Greeting.setProvider(
## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} =
## ok(Greeting(text: "szia"))
## )
##
## let responses = (await Greeting.request()).valueOr(@[Greeting(text: "default")])
##
## echo responses.len
## Greeting.clearProviders()
## ```
## If no `signature` proc is declared, a zero-argument form is generated
## automatically, so the caller only needs to provide the type definition.
import std/[macros, strutils, tables, sugar]
import chronos
import results
import ./helper/broker_utils
import ./broker_context
export results, chronos, broker_context
proc isReturnTypeValid(returnType, typeIdent: NimNode): bool =
## Accept Future[Result[TypeIdent, string]] as the contract.
if returnType.kind != nnkBracketExpr or returnType.len != 2:
return false
if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"):
return false
let inner = returnType[1]
if inner.kind != nnkBracketExpr or inner.len != 3:
return false
if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"):
return false
if inner[1].kind != nnkIdent or not inner[1].eqIdent($typeIdent):
return false
inner[2].kind == nnkIdent and inner[2].eqIdent("string")
proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode =
var formal = newTree(nnkFormalParams)
formal.add(returnType)
for param in params:
formal.add(param)
let pragmas = quote:
{.async.}
newTree(nnkProcTy, formal, pragmas)
macro MultiRequestBroker*(body: untyped): untyped =
when defined(requestBrokerDebug):
echo body.treeRepr
let parsed = parseSingleTypeDef(body, "MultiRequestBroker")
let typeIdent = parsed.typeIdent
let objectDef = parsed.objectDef
let isRefObject = parsed.isRefObject
when defined(requestBrokerDebug):
echo "MultiRequestBroker generating type: ", $typeIdent
let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*")
let sanitized = sanitizeIdentName(typeIdent)
let typeNameLit = newLit($typeIdent)
let isRefObjectLit = newLit(isRefObject)
let uint64Ident = ident("uint64")
let providerKindIdent = ident(sanitized & "ProviderKind")
let providerHandleIdent = ident(sanitized & "ProviderHandle")
let exportedProviderHandleIdent = postfix(copyNimTree(providerHandleIdent), "*")
let bucketTypeIdent = ident(sanitized & "CtxBucket")
let findBucketIdxIdent = ident(sanitized & "FindBucketIdx")
let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx")
let zeroKindIdent = ident("pk" & sanitized & "NoArgs")
let argKindIdent = ident("pk" & sanitized & "WithArgs")
var zeroArgSig: NimNode = nil
var zeroArgProviderName: NimNode = nil
var zeroArgFieldName: NimNode = nil
var argSig: NimNode = nil
var argParams: seq[NimNode] = @[]
var argProviderName: NimNode = nil
var argFieldName: NimNode = nil
for stmt in body:
case stmt.kind
of nnkProcDef:
let procName = stmt[0]
let procNameIdent =
case procName.kind
of nnkIdent:
procName
of nnkPostfix:
procName[1]
else:
procName
let procNameStr = $procNameIdent
if not procNameStr.startsWith("signature"):
error("Signature proc names must start with `signature`", procName)
let params = stmt.params
if params.len == 0:
error("Signature must declare a return type", stmt)
let returnType = params[0]
if not isReturnTypeValid(returnType, typeIdent):
error(
"Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt
)
let paramCount = params.len - 1
if paramCount == 0:
if zeroArgSig != nil:
error("Only one zero-argument signature is allowed", stmt)
zeroArgSig = stmt
zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs")
zeroArgFieldName = ident("providerNoArgs")
elif paramCount >= 1:
if argSig != nil:
error("Only one argument-based signature is allowed", stmt)
argSig = stmt
argParams = @[]
for idx in 1 ..< params.len:
let paramDef = params[idx]
if paramDef.kind != nnkIdentDefs:
error(
"Signature parameter must be a standard identifier declaration", paramDef
)
let paramTypeNode = paramDef[paramDef.len - 2]
if paramTypeNode.kind == nnkEmpty:
error("Signature parameter must declare a type", paramDef)
var hasName = false
for i in 0 ..< paramDef.len - 2:
if paramDef[i].kind != nnkEmpty:
hasName = true
if not hasName:
error("Signature parameter must declare a name", paramDef)
argParams.add(copyNimTree(paramDef))
argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs")
argFieldName = ident("providerWithArgs")
of nnkTypeSection, nnkEmpty:
discard
else:
error("Unsupported statement inside MultiRequestBroker definition", stmt)
if zeroArgSig.isNil() and argSig.isNil():
zeroArgSig = newEmptyNode()
zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs")
zeroArgFieldName = ident("providerNoArgs")
var typeSection = newTree(nnkTypeSection)
typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef))
var kindEnum = newTree(nnkEnumTy, newEmptyNode())
if not zeroArgSig.isNil():
kindEnum.add(zeroKindIdent)
if not argSig.isNil():
kindEnum.add(argKindIdent)
typeSection.add(newTree(nnkTypeDef, providerKindIdent, newEmptyNode(), kindEnum))
var handleRecList = newTree(nnkRecList)
handleRecList.add(newTree(nnkIdentDefs, ident("id"), uint64Ident, newEmptyNode()))
handleRecList.add(
newTree(nnkIdentDefs, ident("kind"), providerKindIdent, newEmptyNode())
)
typeSection.add(
newTree(
nnkTypeDef,
exportedProviderHandleIdent,
newEmptyNode(),
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), handleRecList),
)
)
let returnType = quote:
Future[Result[`typeIdent`, string]]
if not zeroArgSig.isNil():
let procType = makeProcType(returnType, @[])
typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType))
if not argSig.isNil():
let procType = makeProcType(returnType, cloneParams(argParams))
typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType))
var bucketRecList = newTree(nnkRecList)
bucketRecList.add(
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
)
if not zeroArgSig.isNil():
bucketRecList.add(
newTree(
nnkIdentDefs,
zeroArgFieldName,
newTree(nnkBracketExpr, ident("seq"), zeroArgProviderName),
newEmptyNode(),
)
)
if not argSig.isNil():
bucketRecList.add(
newTree(
nnkIdentDefs,
argFieldName,
newTree(nnkBracketExpr, ident("seq"), argProviderName),
newEmptyNode(),
)
)
typeSection.add(
newTree(
nnkTypeDef,
bucketTypeIdent,
newEmptyNode(),
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), bucketRecList),
)
)
var brokerRecList = newTree(nnkRecList)
brokerRecList.add(
newTree(
nnkIdentDefs,
ident("buckets"),
newTree(nnkBracketExpr, ident("seq"), bucketTypeIdent),
newEmptyNode(),
)
)
let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker")
typeSection.add(
newTree(
nnkTypeDef,
brokerTypeIdent,
newEmptyNode(),
newTree(
nnkRefTy, newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList)
),
)
)
result = newStmtList()
result.add(typeSection)
let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker")
let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker")
result.add(
quote do:
var `globalVarIdent` {.threadvar.}: `brokerTypeIdent`
proc `findBucketIdxIdent`(
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
): int =
if brokerCtx == DefaultBrokerContext:
return 0
for i in 1 ..< broker.buckets.len:
if broker.buckets[i].brokerCtx == brokerCtx:
return i
return -1
proc `getOrCreateBucketIdxIdent`(
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
): int =
let idx = `findBucketIdxIdent`(broker, brokerCtx)
if idx >= 0:
return idx
broker.buckets.add(`bucketTypeIdent`(brokerCtx: brokerCtx))
return broker.buckets.high
proc `accessProcIdent`(): `brokerTypeIdent` =
if `globalVarIdent`.isNil():
new(`globalVarIdent`)
`globalVarIdent`.buckets =
@[`bucketTypeIdent`(brokerCtx: DefaultBrokerContext)]
return `globalVarIdent`
)
var clearBody = newStmtList()
if not zeroArgSig.isNil():
result.add(
quote do:
proc setProvider*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
handler: `zeroArgProviderName`,
): Result[`providerHandleIdent`, string] =
if handler.isNil():
return err("Provider handler must be provided")
let broker = `accessProcIdent`()
let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx)
for i, existing in broker.buckets[bucketIdx].`zeroArgFieldName`:
if not existing.isNil() and existing == handler:
return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `zeroKindIdent`))
broker.buckets[bucketIdx].`zeroArgFieldName`.add(handler)
return ok(
`providerHandleIdent`(
id: uint64(broker.buckets[bucketIdx].`zeroArgFieldName`.len),
kind: `zeroKindIdent`,
)
)
proc setProvider*(
_: typedesc[`typeIdent`], handler: `zeroArgProviderName`
): Result[`providerHandleIdent`, string] =
return setProvider(`typeIdent`, DefaultBrokerContext, handler)
)
result.add(
quote do:
proc request*(
_: typedesc[`typeIdent`], brokerCtx: BrokerContext
): Future[Result[seq[`typeIdent`], string]] {.async: (raises: []), gcsafe.} =
var aggregated: seq[`typeIdent`] = @[]
let broker = `accessProcIdent`()
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return ok(aggregated)
let providers = broker.buckets[bucketIdx].`zeroArgFieldName`
if providers.len == 0:
return ok(aggregated)
# var providersFut: seq[Future[Result[`typeIdent`, string]]] = collect:
var providersFut = collect(newSeq):
for provider in providers:
if provider.isNil():
continue
provider()
let catchable = catch:
await allFinished(providersFut)
catchable.isOkOr:
return err("Some provider(s) failed:" & error.msg)
for fut in catchable.get():
if fut.failed():
return err("Some provider(s) failed:" & fut.error.msg)
elif fut.finished():
let providerResult = fut.value()
if providerResult.isOk:
let providerValue = providerResult.get()
when `isRefObjectLit`:
if providerValue.isNil():
return err(
"MultiRequestBroker(" & `typeNameLit` &
"): provider returned nil result"
)
aggregated.add(providerValue)
else:
return err("Some provider(s) failed:" & providerResult.error)
return ok(aggregated)
proc request*(
_: typedesc[`typeIdent`]
): Future[Result[seq[`typeIdent`], string]] =
return request(`typeIdent`, DefaultBrokerContext)
)
if not argSig.isNil():
result.add(
quote do:
proc setProvider*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
handler: `argProviderName`,
): Result[`providerHandleIdent`, string] =
if handler.isNil():
return err("Provider handler must be provided")
let broker = `accessProcIdent`()
let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx)
for i, existing in broker.buckets[bucketIdx].`argFieldName`:
if not existing.isNil() and existing == handler:
return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `argKindIdent`))
broker.buckets[bucketIdx].`argFieldName`.add(handler)
return ok(
`providerHandleIdent`(
id: uint64(broker.buckets[bucketIdx].`argFieldName`.len),
kind: `argKindIdent`,
)
)
proc setProvider*(
_: typedesc[`typeIdent`], handler: `argProviderName`
): Result[`providerHandleIdent`, string] =
return setProvider(`typeIdent`, DefaultBrokerContext, handler)
)
let requestParamDefs = cloneParams(argParams)
let argNameIdents = collectParamNames(requestParamDefs)
let providerSym = genSym(nskLet, "providerVal")
var providerCall = newCall(providerSym)
for argName in argNameIdents:
providerCall.add(argName)
var formalParams = newTree(nnkFormalParams)
formalParams.add(
quote do:
Future[Result[seq[`typeIdent`], string]]
)
formalParams.add(
newTree(
nnkIdentDefs,
ident("_"),
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)),
newEmptyNode(),
)
)
formalParams.add(
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
)
for paramDef in requestParamDefs:
formalParams.add(paramDef)
let requestPragmas = quote:
{.async: (raises: []), gcsafe.}
let requestBody = quote:
var aggregated: seq[`typeIdent`] = @[]
let broker = `accessProcIdent`()
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return ok(aggregated)
let providers = broker.buckets[bucketIdx].`argFieldName`
if providers.len == 0:
return ok(aggregated)
var providersFut = collect(newSeq):
for provider in providers:
if provider.isNil():
continue
let `providerSym` = provider
`providerCall`
let catchable = catch:
await allFinished(providersFut)
catchable.isOkOr:
return err("Some provider(s) failed:" & error.msg)
for fut in catchable.get():
if fut.failed():
return err("Some provider(s) failed:" & fut.error.msg)
elif fut.finished():
let providerResult = fut.value()
if providerResult.isOk:
let providerValue = providerResult.get()
when `isRefObjectLit`:
if providerValue.isNil():
return err(
"MultiRequestBroker(" & `typeNameLit` &
"): provider returned nil result"
)
aggregated.add(providerValue)
else:
return err("Some provider(s) failed:" & providerResult.error)
return ok(aggregated)
result.add(
newTree(
nnkProcDef,
postfix(ident("request"), "*"),
newEmptyNode(),
newEmptyNode(),
formalParams,
requestPragmas,
newEmptyNode(),
requestBody,
)
)
# Backward-compatible default-context overload (no brokerCtx parameter).
var formalParamsDefault = newTree(nnkFormalParams)
formalParamsDefault.add(
quote do:
Future[Result[seq[`typeIdent`], string]]
)
formalParamsDefault.add(
newTree(
nnkIdentDefs,
ident("_"),
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)),
newEmptyNode(),
)
)
for paramDef in requestParamDefs:
formalParamsDefault.add(copyNimTree(paramDef))
var wrapperCall = newCall(ident("request"))
wrapperCall.add(copyNimTree(typeIdent))
wrapperCall.add(ident("DefaultBrokerContext"))
for argName in argNameIdents:
wrapperCall.add(copyNimTree(argName))
result.add(
newTree(
nnkProcDef,
postfix(ident("request"), "*"),
newEmptyNode(),
newEmptyNode(),
formalParamsDefault,
newEmptyNode(),
newEmptyNode(),
newStmtList(newTree(nnkReturnStmt, wrapperCall)),
)
)
let removeHandleCtxSym = genSym(nskParam, "handle")
let removeHandleDefaultSym = genSym(nskParam, "handle")
when true:
# Generate clearProviders / removeProvider with macro-time knowledge about which
# provider lists exist (zero-arg and/or arg providers).
if not zeroArgSig.isNil() and not argSig.isNil():
result.add(
quote do:
proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
let broker = `accessProcIdent`()
if broker.isNil():
return
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0)
broker.buckets[bucketIdx].`argFieldName`.setLen(0)
if brokerCtx != DefaultBrokerContext:
broker.buckets.delete(bucketIdx)
proc clearProviders*(_: typedesc[`typeIdent`]) =
clearProviders(`typeIdent`, DefaultBrokerContext)
proc removeProvider*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
`removeHandleCtxSym`: `providerHandleIdent`,
) =
if `removeHandleCtxSym`.id == 0'u64:
return
let broker = `accessProcIdent`()
if broker.isNil():
return
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
if `removeHandleCtxSym`.kind == `zeroKindIdent`:
let idx = int(`removeHandleCtxSym`.id) - 1
if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len:
broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil
elif `removeHandleCtxSym`.kind == `argKindIdent`:
let idx = int(`removeHandleCtxSym`.id) - 1
if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len:
broker.buckets[bucketIdx].`argFieldName`[idx] = nil
if brokerCtx != DefaultBrokerContext:
var hasAny = false
for p in broker.buckets[bucketIdx].`zeroArgFieldName`:
if not p.isNil():
hasAny = true
break
if not hasAny:
for p in broker.buckets[bucketIdx].`argFieldName`:
if not p.isNil():
hasAny = true
break
if not hasAny:
broker.buckets.delete(bucketIdx)
proc removeProvider*(
_: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent`
) =
removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`)
)
elif not zeroArgSig.isNil():
result.add(
quote do:
proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
let broker = `accessProcIdent`()
if broker.isNil():
return
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0)
if brokerCtx != DefaultBrokerContext:
broker.buckets.delete(bucketIdx)
proc clearProviders*(_: typedesc[`typeIdent`]) =
clearProviders(`typeIdent`, DefaultBrokerContext)
proc removeProvider*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
`removeHandleCtxSym`: `providerHandleIdent`,
) =
if `removeHandleCtxSym`.id == 0'u64:
return
let broker = `accessProcIdent`()
if broker.isNil():
return
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
if `removeHandleCtxSym`.kind != `zeroKindIdent`:
return
let idx = int(`removeHandleCtxSym`.id) - 1
if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len:
broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil
if brokerCtx != DefaultBrokerContext:
var hasAny = false
for p in broker.buckets[bucketIdx].`zeroArgFieldName`:
if not p.isNil():
hasAny = true
break
if not hasAny:
broker.buckets.delete(bucketIdx)
proc removeProvider*(
_: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent`
) =
removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`)
)
else:
result.add(
quote do:
proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
let broker = `accessProcIdent`()
if broker.isNil():
return
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
broker.buckets[bucketIdx].`argFieldName`.setLen(0)
if brokerCtx != DefaultBrokerContext:
broker.buckets.delete(bucketIdx)
proc clearProviders*(_: typedesc[`typeIdent`]) =
clearProviders(`typeIdent`, DefaultBrokerContext)
proc removeProvider*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
`removeHandleCtxSym`: `providerHandleIdent`,
) =
if `removeHandleCtxSym`.id == 0'u64:
return
let broker = `accessProcIdent`()
if broker.isNil():
return
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
if bucketIdx < 0:
return
if `removeHandleCtxSym`.kind != `argKindIdent`:
return
let idx = int(`removeHandleCtxSym`.id) - 1
if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len:
broker.buckets[bucketIdx].`argFieldName`[idx] = nil
if brokerCtx != DefaultBrokerContext:
var hasAny = false
for p in broker.buckets[bucketIdx].`argFieldName`:
if not p.isNil():
hasAny = true
break
if not hasAny:
broker.buckets.delete(bucketIdx)
proc removeProvider*(
_: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent`
) =
removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`)
)
when defined(requestBrokerDebug):
echo result.repr

View File

@ -1,841 +0,0 @@
## RequestBroker
## --------------------
## RequestBroker represents a proactive decoupling pattern, that
## allows defining request-response style interactions between modules without
## need for direct dependencies in between.
## Worth considering using it in a single provider, many requester scenario.
##
## Provides a declarative way to define an immutable value type together with a
## thread-local broker that can register an asynchronous or synchronous provider,
## dispatch typed requests and clear provider.
##
## For consideration use `sync` mode RequestBroker when you need to provide simple value(s)
## where there is no long-running async operation involved.
## Typically it act as a accessor for the local state of generic setting.
##
## `async` mode is better to be used when you request date that may involve some long IO operation
## or action.
##
## Default vs. context aware use:
## Every generated broker is a thread-local global instance. This means each RequestBroker enables decoupled
## data exchange threadwise. Sometimes we use brokers inside a context - like inside a component that has many modules or subsystems.
## In case you would instantiate multiple such components in a single thread, and each component must has its own provider for the same RequestBroker type,
## in order to avoid provider collision, you can use context aware RequestBroker.
## Context awareness is supported through the `BrokerContext` argument for `setProvider`, `request`, `clearProvider` interfaces.
## Suce use requires generating a new unique `BrokerContext` value per component instance, and spread it to all modules using the brokers.
## Example, store the `BrokerContext` as a field inside the top level component instance, and spread around at initialization of the subcomponents..
##
## Default broker context is defined as `DefaultBrokerContext` constant. But if you don't need context awareness, you can use the
## interfaces without context argument.
##
## Usage:
## Declare your desired request type inside a `RequestBroker` macro, add any number of fields.
## Define the provider signature, that is enforced at compile time.
##
## ```nim
## RequestBroker:
## type TypeName = object
## field1*: FieldType
## field2*: AnotherFieldType
##
## proc signature*(): Future[Result[TypeName, string]]
## ## Also possible to define signature with arbitrary input arguments.
## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]]
##
## ```
##
## Sync mode (no `async` / `Future`) can be generated with:
##
## ```nim
## RequestBroker(sync):
## type TypeName = object
## field1*: FieldType
##
## proc signature*(): Result[TypeName, string]
## proc signature*(arg1: ArgType): Result[TypeName, string]
## ```
##
## Note: When the request type is declared as a native type / alias / externally-defined
## type (i.e. not an inline `object` / `ref object` definition), RequestBroker
## will wrap it in `distinct` automatically unless you already used `distinct`.
## This avoids overload ambiguity when multiple brokers share the same
## underlying base type (Nim overload resolution does not consider return type).
##
## This means that for non-object request types you typically:
## - construct values with an explicit cast/constructor, e.g. `MyType("x")`
## - unwrap with a cast when needed, e.g. `string(myVal)` or `BaseType(myVal)`
##
## Example (native response type):
## ```nim
## RequestBroker(sync):
## type MyCount = int # exported as: `distinct int`
##
## MyCount.setProvider(proc(): Result[MyCount, string] = ok(MyCount(42)))
## let res = MyCount.request()
## if res.isOk():
## let raw = int(res.get())
## ```
##
## Example (externally-defined type):
## ```nim
## type External = object
## label*: string
##
## RequestBroker:
## type MyExternal = External # exported as: `distinct External`
##
## MyExternal.setProvider(
## proc(): Future[Result[MyExternal, string]] {.async.} =
## ok(MyExternal(External(label: "hi")))
## )
## let res = await MyExternal.request()
## if res.isOk():
## let base = External(res.get())
## echo base.label
## ```
## The 'TypeName' object defines the requestable data (but also can be seen as request for action with return value).
## The 'signature' proc defines the provider(s) signature, that is enforced at compile time.
## One signature can be with no arguments, another with any number of arguments - where the input arguments are
## not related to the request type - but alternative inputs for the request to be processed.
##
## After this, you can register a provider anywhere in your code with
## `TypeName.setProvider(...)`, which returns error if already having a provider.
## Providers are async procs/lambdas in default mode and sync procs in sync mode.
##
## Providers are stored as a broker-context keyed list:
## - the default provider is always stored at index 0 (reserved broker context: 0)
## - additional providers can be registered under arbitrary non-zero broker contexts
##
## The original `setProvider(handler)` / `request(...)` APIs continue to operate
## on the default provider (broker context 0) for backward compatibility.
##
## Requests can be made from anywhere with no direct dependency on the provider by
## calling `TypeName.request()` - with arguments respecting the signature(s).
## In async mode, this returns a Future[Result[TypeName, string]]. In sync mode, it returns Result[TypeName, string].
##
## Whenever you no want to process requests (or your object instance that provides the request goes out of scope),
## you can remove it from the broker with `TypeName.clearProvider()`.
##
##
## Example:
## ```nim
## RequestBroker:
## type Greeting = object
## text*: string
##
## ## Define the request and provider signature, that is enforced at compile time.
## proc signature*(): Future[Result[Greeting, string]] {.async.}
##
## ## Also possible to define signature with arbitrary input arguments.
## proc signature*(lang: string): Future[Result[Greeting, string]] {.async.}
##
## ...
## Greeting.setProvider(
## proc(): Future[Result[Greeting, string]] {.async.} =
## ok(Greeting(text: "hello"))
## )
## let res = await Greeting.request()
##
##
## ...
## # using native type as response for a synchronous request.
## RequestBroker(sync):
## type NeedThatInfo = string
##
##...
## NeedThatInfo.setProvider(
## proc(): Result[NeedThatInfo, string] =
## ok("this is the info you wanted")
## )
## let res = NeedThatInfo.request().valueOr:
## echo "not ok due to: " & error
## NeedThatInfo(":-(")
##
## echo string(res)
## ```
## If no `signature` proc is declared, a zero-argument form is generated
## automatically, so the caller only needs to provide the type definition.
import std/[macros, strutils]
from std/sequtils import keepItIf
import chronos
import results
import ./helper/broker_utils, broker_context
export results, chronos, keepItIf, broker_context
proc errorFuture[T](message: string): Future[Result[T, string]] {.inline.} =
## Build a future that is already completed with an error result.
let fut = newFuture[Result[T, string]]("request_broker.errorFuture")
fut.complete(err(Result[T, string], message))
fut
type RequestBrokerMode = enum
rbAsync
rbSync
proc isAsyncReturnTypeValid(returnType, typeIdent: NimNode): bool =
## Accept Future[Result[TypeIdent, string]] as the contract.
if returnType.kind != nnkBracketExpr or returnType.len != 2:
return false
if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"):
return false
let inner = returnType[1]
if inner.kind != nnkBracketExpr or inner.len != 3:
return false
if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"):
return false
if inner[1].kind != nnkIdent or not inner[1].eqIdent($typeIdent):
return false
inner[2].kind == nnkIdent and inner[2].eqIdent("string")
proc isSyncReturnTypeValid(returnType, typeIdent: NimNode): bool =
## Accept Result[TypeIdent, string] as the contract.
if returnType.kind != nnkBracketExpr or returnType.len != 3:
return false
if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Result"):
return false
if returnType[1].kind != nnkIdent or not returnType[1].eqIdent($typeIdent):
return false
returnType[2].kind == nnkIdent and returnType[2].eqIdent("string")
proc isReturnTypeValid(returnType, typeIdent: NimNode, mode: RequestBrokerMode): bool =
case mode
of rbAsync:
isAsyncReturnTypeValid(returnType, typeIdent)
of rbSync:
isSyncReturnTypeValid(returnType, typeIdent)
proc makeProcType(
returnType: NimNode, params: seq[NimNode], mode: RequestBrokerMode
): NimNode =
var formal = newTree(nnkFormalParams)
formal.add(returnType)
for param in params:
formal.add(param)
case mode
of rbAsync:
let pragmas = newTree(nnkPragma, ident("async"))
newTree(nnkProcTy, formal, pragmas)
of rbSync:
let raisesPragma = newTree(
nnkExprColonExpr, ident("raises"), newTree(nnkBracket, ident("CatchableError"))
)
let pragmas = newTree(nnkPragma, raisesPragma, ident("gcsafe"))
newTree(nnkProcTy, formal, pragmas)
proc parseMode(modeNode: NimNode): RequestBrokerMode =
## Parses the mode selector for the 2-argument macro overload.
## Supported spellings: `sync` / `async` (case-insensitive).
let raw = ($modeNode).strip().toLowerAscii()
case raw
of "sync":
rbSync
of "async":
rbAsync
else:
error("RequestBroker mode must be `sync` or `async` (default is async)", modeNode)
proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode =
when defined(requestBrokerDebug):
echo body.treeRepr
echo "RequestBroker mode: ", $mode
let parsed = parseSingleTypeDef(body, "RequestBroker", allowRefToNonObject = true)
let typeIdent = parsed.typeIdent
let objectDef = parsed.objectDef
when defined(requestBrokerDebug):
echo "RequestBroker generating type: ", $typeIdent
let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*")
let typeDisplayName = sanitizeIdentName(typeIdent)
let typeNameLit = newLit(typeDisplayName)
var zeroArgSig: NimNode = nil
var zeroArgProviderName: NimNode = nil
var argSig: NimNode = nil
var argParams: seq[NimNode] = @[]
var argProviderName: NimNode = nil
for stmt in body:
case stmt.kind
of nnkProcDef:
let procName = stmt[0]
let procNameIdent =
case procName.kind
of nnkIdent:
procName
of nnkPostfix:
procName[1]
else:
procName
let procNameStr = $procNameIdent
if not procNameStr.startsWith("signature"):
error("Signature proc names must start with `signature`", procName)
let params = stmt.params
if params.len == 0:
error("Signature must declare a return type", stmt)
let returnType = params[0]
if not isReturnTypeValid(returnType, typeIdent, mode):
case mode
of rbAsync:
error(
"Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt
)
of rbSync:
error("Signature must return Result[`" & $typeIdent & "`, string]", stmt)
let paramCount = params.len - 1
if paramCount == 0:
if zeroArgSig != nil:
error("Only one zero-argument signature is allowed", stmt)
zeroArgSig = stmt
zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs")
elif paramCount >= 1:
if argSig != nil:
error("Only one argument-based signature is allowed", stmt)
argSig = stmt
argParams = @[]
for idx in 1 ..< params.len:
let paramDef = params[idx]
if paramDef.kind != nnkIdentDefs:
error(
"Signature parameter must be a standard identifier declaration", paramDef
)
let paramTypeNode = paramDef[paramDef.len - 2]
if paramTypeNode.kind == nnkEmpty:
error("Signature parameter must declare a type", paramDef)
var hasName = false
for i in 0 ..< paramDef.len - 2:
if paramDef[i].kind != nnkEmpty:
hasName = true
if not hasName:
error("Signature parameter must declare a name", paramDef)
argParams.add(copyNimTree(paramDef))
argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs")
of nnkTypeSection, nnkEmpty:
discard
else:
error("Unsupported statement inside RequestBroker definition", stmt)
if zeroArgSig.isNil() and argSig.isNil():
zeroArgSig = newEmptyNode()
zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs")
var typeSection = newTree(nnkTypeSection)
typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef))
let returnType =
case mode
of rbAsync:
quote:
Future[Result[`typeIdent`, string]]
of rbSync:
quote:
Result[`typeIdent`, string]
if not zeroArgSig.isNil():
let procType = makeProcType(returnType, @[], mode)
typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType))
if not argSig.isNil():
let procType = makeProcType(returnType, cloneParams(argParams), mode)
typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType))
var brokerRecList = newTree(nnkRecList)
if not zeroArgSig.isNil():
let zeroArgProvidersFieldName = ident("providersNoArgs")
let zeroArgProvidersTupleTy = newTree(
nnkTupleTy,
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()),
newTree(nnkIdentDefs, ident("handler"), zeroArgProviderName, newEmptyNode()),
)
let zeroArgProvidersSeqTy =
newTree(nnkBracketExpr, ident("seq"), zeroArgProvidersTupleTy)
brokerRecList.add(
newTree(
nnkIdentDefs, zeroArgProvidersFieldName, zeroArgProvidersSeqTy, newEmptyNode()
)
)
if not argSig.isNil():
let argProvidersFieldName = ident("providersWithArgs")
let argProvidersTupleTy = newTree(
nnkTupleTy,
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()),
newTree(nnkIdentDefs, ident("handler"), argProviderName, newEmptyNode()),
)
let argProvidersSeqTy = newTree(nnkBracketExpr, ident("seq"), argProvidersTupleTy)
brokerRecList.add(
newTree(nnkIdentDefs, argProvidersFieldName, argProvidersSeqTy, newEmptyNode())
)
let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker")
let brokerTypeDef = newTree(
nnkTypeDef,
brokerTypeIdent,
newEmptyNode(),
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList),
)
typeSection.add(brokerTypeDef)
result = newStmtList()
result.add(typeSection)
let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker")
let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker")
var brokerNewBody = newStmtList()
if not zeroArgSig.isNil():
brokerNewBody.add(
quote do:
result.providersNoArgs =
@[(brokerCtx: DefaultBrokerContext, handler: default(`zeroArgProviderName`))]
)
if not argSig.isNil():
brokerNewBody.add(
quote do:
result.providersWithArgs =
@[(brokerCtx: DefaultBrokerContext, handler: default(`argProviderName`))]
)
var brokerInitChecks = newStmtList()
if not zeroArgSig.isNil():
brokerInitChecks.add(
quote do:
if `globalVarIdent`.providersNoArgs.len == 0:
`globalVarIdent` = `brokerTypeIdent`.new()
)
if not argSig.isNil():
brokerInitChecks.add(
quote do:
if `globalVarIdent`.providersWithArgs.len == 0:
`globalVarIdent` = `brokerTypeIdent`.new()
)
result.add(
quote do:
var `globalVarIdent` {.threadvar.}: `brokerTypeIdent`
proc new(_: type `brokerTypeIdent`): `brokerTypeIdent` =
result = `brokerTypeIdent`()
`brokerNewBody`
proc `accessProcIdent`(): var `brokerTypeIdent` =
`brokerInitChecks`
`globalVarIdent`
)
var clearBodyKeyed = newStmtList()
let brokerCtxParamIdent = ident("brokerCtx")
if not zeroArgSig.isNil():
let zeroArgProvidersFieldName = ident("providersNoArgs")
result.add(
quote do:
proc setProvider*(
_: typedesc[`typeIdent`], handler: `zeroArgProviderName`
): Result[void, string] =
if not `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler.isNil():
return err("Zero-arg provider already set")
`accessProcIdent`().`zeroArgProvidersFieldName`[0].handler = handler
return ok()
)
result.add(
quote do:
proc setProvider*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
handler: `zeroArgProviderName`,
): Result[void, string] =
if brokerCtx == DefaultBrokerContext:
return setProvider(`typeIdent`, handler)
for entry in `accessProcIdent`().`zeroArgProvidersFieldName`:
if entry.brokerCtx == brokerCtx:
return err(
"RequestBroker(" & `typeNameLit` &
"): provider already set for broker context " & $brokerCtx
)
`accessProcIdent`().`zeroArgProvidersFieldName`.add(
(brokerCtx: brokerCtx, handler: handler)
)
return ok()
)
clearBodyKeyed.add(
quote do:
if `brokerCtxParamIdent` == DefaultBrokerContext:
`accessProcIdent`().`zeroArgProvidersFieldName`[0].handler =
default(`zeroArgProviderName`)
else:
`accessProcIdent`().`zeroArgProvidersFieldName`.keepItIf(
it.brokerCtx != `brokerCtxParamIdent`
)
)
case mode
of rbAsync:
result.add(
quote do:
proc request*(
_: typedesc[`typeIdent`]
): Future[Result[`typeIdent`, string]] {.async: (raises: []).} =
return await request(`typeIdent`, DefaultBrokerContext)
)
result.add(
quote do:
proc request*(
_: typedesc[`typeIdent`], brokerCtx: BrokerContext
): Future[Result[`typeIdent`, string]] {.async: (raises: []).} =
var provider: `zeroArgProviderName`
if brokerCtx == DefaultBrokerContext:
provider = `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler
else:
for entry in `accessProcIdent`().`zeroArgProvidersFieldName`:
if entry.brokerCtx == brokerCtx:
provider = entry.handler
break
if provider.isNil():
if brokerCtx == DefaultBrokerContext:
return err(
"RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered"
)
return err(
"RequestBroker(" & `typeNameLit` &
"): no provider registered for broker context " & $brokerCtx
)
let catchedRes = catch:
await provider()
if catchedRes.isErr():
return err(
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " &
catchedRes.error.msg
)
let providerRes = catchedRes.get()
if providerRes.isOk():
let resultValue = providerRes.get()
when compiles(resultValue.isNil()):
if resultValue.isNil():
return err(
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
)
return providerRes
)
of rbSync:
result.add(
quote do:
proc request*(
_: typedesc[`typeIdent`]
): Result[`typeIdent`, string] {.gcsafe, raises: [].} =
return request(`typeIdent`, DefaultBrokerContext)
)
result.add(
quote do:
proc request*(
_: typedesc[`typeIdent`], brokerCtx: BrokerContext
): Result[`typeIdent`, string] {.gcsafe, raises: [].} =
var provider: `zeroArgProviderName`
if brokerCtx == DefaultBrokerContext:
provider = `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler
else:
for entry in `accessProcIdent`().`zeroArgProvidersFieldName`:
if entry.brokerCtx == brokerCtx:
provider = entry.handler
break
if provider.isNil():
if brokerCtx == DefaultBrokerContext:
return err(
"RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered"
)
return err(
"RequestBroker(" & `typeNameLit` &
"): no provider registered for broker context " & $brokerCtx
)
var providerRes: Result[`typeIdent`, string]
try:
providerRes = provider()
except CatchableError as e:
return err(
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " &
e.msg
)
if providerRes.isOk():
let resultValue = providerRes.get()
when compiles(resultValue.isNil()):
if resultValue.isNil():
return err(
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
)
return providerRes
)
if not argSig.isNil():
let argProvidersFieldName = ident("providersWithArgs")
result.add(
quote do:
proc setProvider*(
_: typedesc[`typeIdent`], handler: `argProviderName`
): Result[void, string] =
if not `accessProcIdent`().`argProvidersFieldName`[0].handler.isNil():
return err("Provider already set")
`accessProcIdent`().`argProvidersFieldName`[0].handler = handler
return ok()
)
result.add(
quote do:
proc setProvider*(
_: typedesc[`typeIdent`],
brokerCtx: BrokerContext,
handler: `argProviderName`,
): Result[void, string] =
if brokerCtx == DefaultBrokerContext:
return setProvider(`typeIdent`, handler)
for entry in `accessProcIdent`().`argProvidersFieldName`:
if entry.brokerCtx == brokerCtx:
return err(
"RequestBroker(" & `typeNameLit` &
"): provider already set for broker context " & $brokerCtx
)
`accessProcIdent`().`argProvidersFieldName`.add(
(brokerCtx: brokerCtx, handler: handler)
)
return ok()
)
clearBodyKeyed.add(
quote do:
if `brokerCtxParamIdent` == DefaultBrokerContext:
`accessProcIdent`().`argProvidersFieldName`[0].handler =
default(`argProviderName`)
else:
`accessProcIdent`().`argProvidersFieldName`.keepItIf(
it.brokerCtx != `brokerCtxParamIdent`
)
)
let requestParamDefs = cloneParams(argParams)
let argNameIdents = collectParamNames(requestParamDefs)
var formalParams = newTree(nnkFormalParams)
formalParams.add(copyNimTree(returnType))
formalParams.add(
newTree(
nnkIdentDefs,
ident("_"),
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)),
newEmptyNode(),
)
)
for paramDef in requestParamDefs:
formalParams.add(paramDef)
let requestPragmas =
case mode
of rbAsync:
quote:
{.async: (raises: []).}
of rbSync:
quote:
{.gcsafe, raises: [].}
var forwardCall = newCall(ident("request"))
forwardCall.add(copyNimTree(typeIdent))
forwardCall.add(ident("DefaultBrokerContext"))
for argName in argNameIdents:
forwardCall.add(argName)
var requestBody = newStmtList()
case mode
of rbAsync:
requestBody.add(
quote do:
return await `forwardCall`
)
of rbSync:
requestBody.add(
quote do:
return `forwardCall`
)
result.add(
newTree(
nnkProcDef,
postfix(ident("request"), "*"),
newEmptyNode(),
newEmptyNode(),
formalParams,
requestPragmas,
newEmptyNode(),
requestBody,
)
)
# Keyed request variant for the argument-based signature.
let requestParamDefsKeyed = cloneParams(argParams)
let argNameIdentsKeyed = collectParamNames(requestParamDefsKeyed)
let providerSymKeyed = genSym(nskVar, "provider")
var formalParamsKeyed = newTree(nnkFormalParams)
formalParamsKeyed.add(copyNimTree(returnType))
formalParamsKeyed.add(
newTree(
nnkIdentDefs,
ident("_"),
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)),
newEmptyNode(),
)
)
formalParamsKeyed.add(
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
)
for paramDef in requestParamDefsKeyed:
formalParamsKeyed.add(paramDef)
let requestPragmasKeyed = requestPragmas
var providerCallKeyed = newCall(providerSymKeyed)
for argName in argNameIdentsKeyed:
providerCallKeyed.add(argName)
var requestBodyKeyed = newStmtList()
requestBodyKeyed.add(
quote do:
var `providerSymKeyed`: `argProviderName`
if brokerCtx == DefaultBrokerContext:
`providerSymKeyed` = `accessProcIdent`().`argProvidersFieldName`[0].handler
else:
for entry in `accessProcIdent`().`argProvidersFieldName`:
if entry.brokerCtx == brokerCtx:
`providerSymKeyed` = entry.handler
break
)
requestBodyKeyed.add(
quote do:
if `providerSymKeyed`.isNil():
if brokerCtx == DefaultBrokerContext:
return err(
"RequestBroker(" & `typeNameLit` &
"): no provider registered for input signature"
)
return err(
"RequestBroker(" & `typeNameLit` &
"): no provider registered for broker context " & $brokerCtx
)
)
case mode
of rbAsync:
requestBodyKeyed.add(
quote do:
let catchedRes = catch:
await `providerCallKeyed`
if catchedRes.isErr():
return err(
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " &
catchedRes.error.msg
)
let providerRes = catchedRes.get()
if providerRes.isOk():
let resultValue = providerRes.get()
when compiles(resultValue.isNil()):
if resultValue.isNil():
return err(
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
)
return providerRes
)
of rbSync:
requestBodyKeyed.add(
quote do:
var providerRes: Result[`typeIdent`, string]
try:
providerRes = `providerCallKeyed`
except CatchableError as e:
return err(
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " & e.msg
)
if providerRes.isOk():
let resultValue = providerRes.get()
when compiles(resultValue.isNil()):
if resultValue.isNil():
return err(
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
)
return providerRes
)
result.add(
newTree(
nnkProcDef,
postfix(ident("request"), "*"),
newEmptyNode(),
newEmptyNode(),
formalParamsKeyed,
requestPragmasKeyed,
newEmptyNode(),
requestBodyKeyed,
)
)
block:
var formalParamsClearKeyed = newTree(nnkFormalParams)
formalParamsClearKeyed.add(newEmptyNode())
formalParamsClearKeyed.add(
newTree(
nnkIdentDefs,
ident("_"),
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)),
newEmptyNode(),
)
)
formalParamsClearKeyed.add(
newTree(nnkIdentDefs, brokerCtxParamIdent, ident("BrokerContext"), newEmptyNode())
)
result.add(
newTree(
nnkProcDef,
postfix(ident("clearProvider"), "*"),
newEmptyNode(),
newEmptyNode(),
formalParamsClearKeyed,
newEmptyNode(),
newEmptyNode(),
clearBodyKeyed,
)
)
result.add(
quote do:
proc clearProvider*(_: typedesc[`typeIdent`]) =
clearProvider(`typeIdent`, DefaultBrokerContext)
)
when defined(requestBrokerDebug):
echo result.repr
return result
macro RequestBroker*(body: untyped): untyped =
## Default (async) mode.
generateRequestBroker(body, rbAsync)
macro RequestBroker*(mode: untyped, body: untyped): untyped =
## Explicit mode selector.
## Example:
## RequestBroker(sync):
## type Foo = object
## proc signature*(): Result[Foo, string]
generateRequestBroker(body, parseMode(mode))

View File

@ -10,7 +10,7 @@ import
eth/keys as eth_keys,
eth/p2p/discoveryv5/node,
eth/p2p/discoveryv5/protocol
import ../node/peer_manager/peer_manager, ../waku_core, ../waku_enr
import waku/[net/auto_port, node/peer_manager/peer_manager, waku_core, waku_enr]
export protocol, waku_enr
@ -409,7 +409,15 @@ proc setupDiscoveryV5*(
key: crypto.PrivateKey,
p2pListenAddress: IpAddress,
portsShift: uint16,
): WakuDiscoveryV5 =
): Result[WakuDiscoveryV5, string] =
## Public only for testing. Callers should use `setupAndStartDiscv5`, which
## additionally handles `udpPort == 0` via auto-port retry.
if conf.udpPort == Port(0):
return err(
"setupDiscoveryV5: udpPort must be non-zero; " &
"use setupAndStartDiscv5 for port=0 auto-port retry"
)
let dynamicBootstrapEnrs =
dynamicBootstrapNodes.filterIt(it.hasUdpPort()).mapIt(it.enr.get())
@ -441,10 +449,47 @@ proc setupDiscoveryV5*(
autoupdateRecord: conf.enrAutoUpdate,
)
WakuDiscoveryV5.new(
rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue
return ok(
WakuDiscoveryV5.new(
rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue
)
)
proc setupAndStartDiscv5*(
myENR: enr.Record,
nodePeerManager: PeerManager,
nodeTopicSubscriptionQueue: AsyncEventQueue[SubscriptionEvent],
conf: Discv5Conf,
dynamicBootstrapNodes: seq[RemotePeerInfo],
rng: ref HmacDrbgContext,
key: crypto.PrivateKey,
p2pListenAddress: IpAddress,
portsShift: uint16,
): Future[Result[WakuDiscoveryV5, string]] {.async: (raises: []).} =
## Construct and start a `WakuDiscoveryV5` instance, handling auto-port
## retry when the caller asks for `udpPort == 0`.
proc attempt(
port: Port
): Future[Result[WakuDiscoveryV5, string]] {.async: (raises: []).} =
var c = conf
c.udpPort = port
let wd = setupDiscoveryV5(
myENR, nodePeerManager, nodeTopicSubscriptionQueue, c, dynamicBootstrapNodes, rng,
key, p2pListenAddress, portsShift,
).valueOr:
return err(error)
let startRes = await wd.start()
if startRes.isErr():
return err("failed to start discovery, attempt: " & startRes.error)
return ok(wd)
let wd = (await tryWithAutoPort[WakuDiscoveryV5](conf.udpPort, attempt)).valueOr:
return err("setupAndStartDiscv5: " & error)
return ok(wd)
proc udpPort*(wd: WakuDiscoveryV5): Port =
wd.conf.port
proc updateBootstrapRecords*(
self: var WakuDiscoveryV5, newRecordsString: string
): Result[void, string] =

View File

@ -1,4 +1,5 @@
import waku/waku_core/[message/message, message/digest], waku/common/broker/event_broker
import brokers/event_broker
import waku/waku_core/[message/message, message/digest]
EventBroker:
type OnFilterSubscribeEvent* = object

View File

@ -1,3 +1,3 @@
import ./[message_events, delivery_events, health_events, peer_events]
import ./[message_events, delivery_events, health_events, peer_events, lifecycle_events]
export message_events, delivery_events, health_events, peer_events
export message_events, delivery_events, health_events, peer_events, lifecycle_events

View File

@ -1,4 +1,4 @@
import waku/common/broker/event_broker
import brokers/event_broker
import waku/api/types
import waku/node/health_monitor/[protocol_health, topic_health]

View File

@ -1,5 +1,5 @@
import waku/[api/types, waku_core/message, waku_core/topics, common/broker/event_broker]
import brokers/event_broker
import waku/[api/types, waku_core/message, waku_core/topics]
export types
EventBroker:

View File

@ -1,4 +1,4 @@
import waku/common/broker/event_broker
import brokers/event_broker
import libp2p/switch
type WakuPeerEventKind* {.pure.} = enum

View File

@ -8,15 +8,16 @@ import
libp2p/builders,
libp2p/nameresolving/nameresolver,
libp2p/transports/wstransport,
libp2p/protocols/connectivity/relay/relay
libp2p/protocols/connectivity/relay/relay,
brokers/broker_context
import
../waku_enr,
../discovery/waku_discv5,
../waku_node,
../node/peer_manager,
../common/rate_limit/setting,
../common/utils/parse_size_units,
../common/broker/broker_context
../common/utils/parse_size_units
type
WakuNodeBuilder* = object # General

View File

@ -4,6 +4,8 @@ import ../waku_conf
logScope:
topics = "waku conf builder discv5"
const DefaultDiscv5UdpPort*: Port = Port(9000)
###########################
## Discv5 Config Builder ##
###########################
@ -38,8 +40,8 @@ proc withTableIpLimit*(b: var Discv5ConfBuilder, tableIpLimit: uint) =
proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: Port) =
b.udpPort = some(udpPort)
proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint) =
b.udpPort = some(Port(udpPort.uint16))
proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint16) =
b.udpPort = some(Port(udpPort))
proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string]) =
# TODO: validate ENRs?
@ -57,7 +59,7 @@ proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] =
bucketIpLimit: b.bucketIpLimit.get(2),
enrAutoUpdate: b.enrAutoUpdate.get(true),
tableIpLimit: b.tableIpLimit.get(10),
udpPort: b.udpPort.get(9000.Port),
udpPort: b.udpPort.get(DefaultDiscv5UdpPort),
)
)
)

View File

@ -4,6 +4,8 @@ import ../waku_conf
logScope:
topics = "waku conf builder metrics server"
const DefaultMetricsHttpPort*: Port = Port(8008)
###################################
## Metrics Server Config Builder ##
###################################
@ -40,7 +42,7 @@ proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], stri
some(
MetricsServerConf(
httpAddress: b.httpAddress.get(static parseIpAddress("127.0.0.1")),
httpPort: b.httpPort.get(8008.Port),
httpPort: b.httpPort.get(DefaultMetricsHttpPort),
logging: b.logging.get(false),
)
)

View File

@ -4,6 +4,8 @@ import ../waku_conf
logScope:
topics = "waku conf builder rest server"
const DefaultRestPort*: Port = Port(8645)
################################
## REST Server Config Builder ##
################################
@ -46,8 +48,6 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] =
if b.listenAddress.isNone():
return err("restServer.listenAddress is not specified")
if b.port.isNone():
return err("restServer.port is not specified")
if b.relayCacheCapacity.isNone():
return err("restServer.relayCacheCapacity is not specified")
@ -56,7 +56,7 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] =
RestServerConf(
allowOrigin: b.allowOrigin,
listenAddress: b.listenAddress.get(),
port: b.port.get(),
port: b.port.get(DefaultRestPort),
admin: b.admin.get(false),
relayCacheCapacity: b.relayCacheCapacity.get(),
)

View File

@ -8,11 +8,14 @@ import
results
import
../waku_conf,
../networks_config,
../../common/logging,
../../common/utils/parse_size_units,
../../waku_enr/capabilities,
waku/[
factory/waku_conf,
factory/networks_config,
common/logging,
common/utils/parse_size_units,
waku_enr/capabilities,
persistency/persistency,
],
tools/confutils/entry_nodes
import
@ -32,7 +35,9 @@ import
logScope:
topics = "waku conf builder"
const DefaultMaxConnections* = 150
const
DefaultMaxConnections* = 150
DefaultP2pTcpPort*: Port = Port(60000)
type MaxMessageSizeKind* = enum
mmskNone
@ -132,6 +137,8 @@ type WakuConfBuilder* = object
circuitRelayClient: Option[bool]
p2pReliability: Option[bool]
localStoragePath: Option[string]
proc init*(T: type WakuConfBuilder): WakuConfBuilder =
WakuConfBuilder(
dnsDiscoveryConf: DnsDiscoveryConfBuilder.init(),
@ -268,6 +275,9 @@ proc withRelayShardedPeerManagement*(
proc withP2pReliability*(b: var WakuConfBuilder, p2pReliability: bool) =
b.p2pReliability = some(p2pReliability)
proc withLocalStoragePath*(b: var WakuConfBuilder, localStoragePath: string) =
b.localStoragePath = some(localStoragePath)
proc withExtMultiAddrs*(builder: var WakuConfBuilder, extMultiAddrs: seq[string]) =
builder.extMultiAddrs = concat(builder.extMultiAddrs, extMultiAddrs)
@ -574,12 +584,7 @@ proc build*(
warn "Nat Strategy is not specified, defaulting to none"
"none"
let p2pTcpPort =
if builder.p2pTcpPort.isSome():
builder.p2pTcpPort.get()
else:
warn "P2P Listening TCP Port is not specified, listening on 60000"
60000.Port
let p2pTcpPort = builder.p2pTcpPort.get(DefaultP2pTcpPort)
let p2pListenAddress =
if builder.p2pListenAddress.isSome():
@ -720,6 +725,7 @@ proc build*(
relayShardedPeerManagement: relayShardedPeerManagement,
p2pReliability: builder.p2pReliability.get(false),
wakuFlags: wakuFlags,
localStoragePath: builder.localStoragePath.get(DefaultStoragePath),
)
?wakuConf.validate()

View File

@ -4,6 +4,8 @@ import waku/factory/waku_conf
logScope:
topics = "waku conf builder websocket"
const DefaultWebSocketPort*: Port = Port(8000)
##############################
## WebSocket Config Builder ##
##############################
@ -41,14 +43,12 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] =
if not b.enabled.get(false):
return ok(none(WebSocketConf))
if b.webSocketPort.isNone():
return err("websocket.port is not specified")
if not b.secureEnabled.get(false):
return ok(
some(
WebSocketConf(
port: b.websocketPort.get(), secureConf: none(WebSocketSecureConf)
port: b.webSocketPort.get(DefaultWebSocketPort),
secureConf: none(WebSocketSecureConf),
)
)
)
@ -61,7 +61,7 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] =
return ok(
some(
WebSocketConf(
port: b.webSocketPort.get(),
port: b.webSocketPort.get(DefaultWebSocketPort),
secureConf: some(
WebSocketSecureConf(keyPath: b.keyPath.get(), certPath: b.certPath.get())
),

View File

@ -8,10 +8,10 @@ import
std/[options, sequtils, net],
results
import ../common/utils/nat, ../node/net_config, ../waku_enr, ../waku_core, ./waku_conf
import waku/[common/utils/nat, net/net_config, waku_enr, waku_core], ./waku_conf
proc enrConfiguration*(
conf: WakuConf, netConfig: NetConfig
proc tryBuildEnrRecord(
conf: WakuConf, netConfig: NetConfig, multiaddrs: seq[MultiAddress]
): Result[enr.Record, string] =
var enrBuilder = EnrBuilder.init(conf.nodeKey)
@ -22,7 +22,8 @@ proc enrConfiguration*(
if netConfig.wakuFlags.isSome():
enrBuilder.withWakuCapabilities(netConfig.wakuFlags.get())
enrBuilder.withMultiaddrs(netConfig.enrMultiaddrs)
if multiaddrs.len > 0:
enrBuilder.withMultiaddrs(multiaddrs)
enrBuilder.withWakuRelaySharding(
RelayShards(clusterId: conf.clusterId, shardIds: conf.subscribeShards)
@ -30,11 +31,35 @@ proc enrConfiguration*(
return err("could not initialize ENR with shards")
let record = enrBuilder.build().valueOr:
error "failed to create enr record", error = error
return err($error)
return ok(record)
proc enrConfiguration*(
conf: WakuConf, netConfig: NetConfig
): Result[enr.Record, string] =
for retained in countdown(netConfig.enrMultiaddrs.len, 0):
let multiaddrs = netConfig.enrMultiaddrs[0 ..< retained]
let record = tryBuildEnrRecord(conf, netConfig, multiaddrs).valueOr:
if retained > 0:
warn "failed to create enr record, retrying with fewer multiaddrs",
error = error,
totalMultiaddrs = netConfig.enrMultiaddrs.len,
retainedMultiaddrs = retained - 1,
removedMultiaddr = multiaddrs[^1]
continue
error "failed to create enr record", error = error
return err($error)
if retained < netConfig.enrMultiaddrs.len:
warn "created enr record after trimming multiaddrs",
totalMultiaddrs = netConfig.enrMultiaddrs.len, retainedMultiaddrs = retained
return ok(record)
return err("failed to create enr record")
proc dnsResolve*(
domain: string, dnsAddrsNameServers: seq[IpAddress]
): Future[Result[string, string]] {.async.} =

View File

@ -18,6 +18,7 @@ import
presto,
metrics,
metrics/chronos_httpserver,
brokers/broker_context,
waku/[
waku_core,
waku_node,
@ -30,7 +31,6 @@ import
waku_enr/multiaddr,
api/types,
common/logging,
common/broker/broker_context,
node/peer_manager,
node/health_monitor,
node/waku_metrics,
@ -46,6 +46,7 @@ import
factory/node_factory,
factory/internal_config,
factory/app_callbacks,
persistency/persistency,
],
./waku_conf,
./waku_state_info
@ -202,6 +203,11 @@ proc new*(
else:
nil
if not restServer.isNil():
let boundRestPort = restServer.httpServer.address.port
node.ports.rest = boundRestPort.uint16
wakuConf.restServerConf.get().port = boundRestPort
# Set the extMultiAddrsOnly flag so the node knows not to replace explicit addresses
node.extMultiAddrsOnly = wakuConf.endpointConf.extMultiAddrsOnly
@ -249,7 +255,7 @@ proc getPorts(
return ok((tcpPort: tcpPort, websocketPort: websocketPort))
proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.async.} =
var conf = waku[].conf
let conf = waku[].conf
let (tcpPort, websocketPort) = getPorts(waku[].node.switch.peerInfo.listenAddrs).valueOr:
return err("Could not retrieve ports: " & error)
@ -281,6 +287,10 @@ proc updateEnr(waku: ptr Waku): Future[Result[void, string]] {.async.} =
waku[].node.enr = record
# If TCP/WS was configured with port 0, node.announcedAddresses was built
# pre-bind with a port value of 0. In any case, the resync is harmless.
waku[].node.announcedAddresses = netConf.announcedAddresses
return ok()
proc updateAddressInENR(waku: ptr Waku): Result[void, string] =
@ -312,11 +322,8 @@ proc updateAddressInENR(waku: ptr Waku): Result[void, string] =
return ok()
proc updateWaku(waku: ptr Waku): Future[Result[void, string]] {.async.} =
let conf = waku[].conf
if conf.endpointConf.p2pTcpPort == Port(0) or
(conf.websocketConf.isSome() and conf.websocketConf.get.port == Port(0)):
(await updateEnr(waku)).isOkOr:
return err("error calling updateEnr: " & $error)
(await updateEnr(waku)).isOkOr:
return err("error calling updateEnr: " & $error)
?updateAnnouncedAddrWithPrimaryIpAddr(waku[].node)
@ -387,32 +394,46 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
else:
waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get()
## Initialize persistency singleton instance - we don't need the instance itself here,
## but this ensures it's initialized before any store job starts.
discard Persistency.instance(conf.localStoragePath).valueOr:
error "Failed to initialize persistency instance", error = $error
return err("Failed to initialize persistency instance: " & $error)
(await startNode(waku.node, waku.conf, waku.dynamicBootstrapNodes)).isOkOr:
return err("error while calling startNode: " & $error)
let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr:
return err("failed to read bound ports from switch: " & $error)
waku[].node.ports.tcp = bound.tcpPort.get(Port(0)).uint16
waku[].node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16
## Discv5
if conf.discv5Conf.isSome():
waku[].wakuDiscV5 = (
await waku_discv5.setupAndStartDiscv5(
waku.node.enr,
waku.node.peerManager,
waku.node.topicSubscriptionQueue,
conf.discv5Conf.get(),
waku.dynamicBootstrapNodes,
waku.rng,
conf.nodeKey,
conf.endpointConf.p2pListenAddress,
conf.portsShift,
)
).valueOr:
return err("failed to start waku discovery v5: " & error)
waku[].node.ports.discv5Udp = waku[].wakuDiscV5.udpPort.uint16
waku[].conf.discv5Conf.get().udpPort = waku[].wakuDiscV5.udpPort
## Update waku data that is set dynamically on node start
try:
(await updateWaku(waku)).isOkOr:
return err("Error in updateApp: " & $error)
return err("Error in startWaku: " & $error)
except CatchableError:
return err("Caught exception in updateApp: " & getCurrentExceptionMsg())
## Discv5
if conf.discv5Conf.isSome():
waku[].wakuDiscV5 = waku_discv5.setupDiscoveryV5(
waku.node.enr,
waku.node.peerManager,
waku.node.topicSubscriptionQueue,
conf.discv5Conf.get(),
waku.dynamicBootstrapNodes,
waku.rng,
conf.nodeKey,
conf.endpointConf.p2pListenAddress,
conf.portsShift,
)
(await waku.wakuDiscV5.start()).isOkOr:
return err("failed to start waku discovery v5: " & $error)
return err("Caught exception in startWaku: " & getCurrentExceptionMsg())
## Reliability
if not waku[].deliveryService.isNil():
@ -482,14 +503,15 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
if conf.metricsServerConf.isSome():
try:
waku[].metricsServer = (
await (
waku_metrics.startMetricsServerAndLogging(
conf.metricsServerConf.get(), conf.portsShift
)
let (server, port) = (
await waku_metrics.startMetricsServerAndLogging(
conf.metricsServerConf.get(), conf.portsShift
)
).valueOr:
return err("Starting monitoring and external interfaces failed: " & error)
waku[].metricsServer = server
waku[].node.ports.metrics = port.uint16
waku[].conf.metricsServerConf.get().httpPort = port
except CatchableError:
return err(
"Caught exception starting monitoring and external interfaces failed: " &
@ -508,6 +530,8 @@ proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} =
try:
waku.healthMonitor.setOverallHealth(HealthStatus.SHUTTING_DOWN)
Persistency.reset()
if not waku.metricsServer.isNil():
await waku.metricsServer.stop()

View File

@ -152,6 +152,8 @@ type WakuConf* {.requiresInit.} = ref object
p2pReliability*: bool
localStoragePath*: string
proc logConf*(conf: WakuConf) =
info "Configuration: Enabled protocols",
relay = conf.relay,

View File

@ -5,8 +5,8 @@
## accessible through the debug API.
import std/[tables, sequtils, strutils]
import metrics, eth/p2p/discoveryv5/enr, libp2p/peerid
import waku/waku_node
import metrics, eth/p2p/discoveryv5/enr, libp2p/peerid, stew/byteutils
import waku/[waku_node, net/bound_ports]
type
NodeInfoId* {.pure.} = enum
@ -15,6 +15,8 @@ type
MyMultiaddresses
MyENR
MyPeerId
MyBoundPorts
MyMixPubKey
WakuStateInfo* {.requiresInit.} = object
node: WakuNode
@ -43,6 +45,13 @@ proc getNodeInfoItem*(self: WakuStateInfo, infoItemId: NodeInfoId): string =
return self.node.enr.toURI()
of NodeInfoId.MyPeerId:
return $PeerId(self.node.peerId())
of NodeInfoId.MyBoundPorts:
return $self.node.ports
of NodeInfoId.MyMixPubKey:
## Empty when the mix protocol is not mounted on this node.
if self.node.wakuMix.isNil():
return ""
return self.node.wakuMix.pubKey.to0xHex()
else:
return "unknown info item id"

48
waku/net/auto_port.nim Normal file
View File

@ -0,0 +1,48 @@
{.push raises: [].}
import std/[net, random]
import chronos, results
const
AutoPortRetryCount* = 20
AutoPortMin = 50000'u16
AutoPortMax = 59000'u16
AutoPortAttemptTimeout = chronos.seconds(30)
proc getAutoPort*(): uint16 =
var rng = initRand()
uint16(rng.rand(AutoPortMin.int .. AutoPortMax.int))
proc tryWithAutoPort*[T](
startingPort: Port,
attempt: proc(p: Port): Future[Result[T, string]] {.async: (raises: []).},
): Future[Result[T, string]] {.async: (raises: []).} =
## If `startingPort == Port(0)`, call `attempt` up to `AutoPortRetryCount`
## times with random ports. Otherwise call it once with `startingPort`.
## Returns the first ok or the last err.
let autoMode = startingPort == Port(0)
let attempts = if autoMode: AutoPortRetryCount else: 1
var lastErr = ""
for i in 1 .. attempts:
let port =
if autoMode:
Port(getAutoPort())
else:
startingPort
let fut = attempt(port)
let res =
try:
if await fut.withTimeout(AutoPortAttemptTimeout):
await fut
else:
fut.cancelSoon()
Result[T, string].err("bind attempt timed out")
except CancelledError:
fut.cancelSoon()
Result[T, string].err("bind attempt cancelled")
if res.isOk():
return ok(res.get())
lastErr = res.error
if autoMode:
return err("auto-port exhausted; last error: " & lastErr)
return err("port bind failed: " & lastErr)

20
waku/net/bound_ports.nim Normal file
View File

@ -0,0 +1,20 @@
{.push raises: [].}
import std/json
type BoundPorts* {.requiresInit.} = object
## Set by the factory once each service has bound to a port.
## A value of 0 means the service was not enabled or did not bind.
tcp*: uint16
webSocket*: uint16
rest*: uint16
discv5Udp*: uint16
metrics*: uint16
proc init*(T: type BoundPorts): BoundPorts =
return BoundPorts(
tcp: 0'u16, webSocket: 0'u16, rest: 0'u16, discv5Udp: 0'u16, metrics: 0'u16
)
proc `$`*(p: BoundPorts): string =
return $(%*p)

View File

@ -156,12 +156,16 @@ proc init*(
if extMultiAddrs.len > 0:
announcedAddresses.add(extMultiAddrs)
announcedAddresses = announcedAddresses.deduplicate()
let
# enrMultiaddrs are just addresses which cannot be represented in ENR, as described in
# https://rfc.vac.dev/spec/31/#many-connection-types
enrMultiaddrs = announcedAddresses.filterIt(
it.hasProtocol("dns4") or it.hasProtocol("dns6") or it.hasProtocol("ws") or
it.hasProtocol("wss")
enrMultiaddrs = deduplicate(
announcedAddresses.filterIt(
it.hasProtocol("dns4") or it.hasProtocol("dns6") or it.hasProtocol("ws") or
it.hasProtocol("wss")
)
)
ok(

View File

@ -5,6 +5,7 @@
import std/[tables, sequtils, options, sets]
import chronos, chronicles, libp2p/utility
import ../[subscription_manager]
import brokers/broker_context
import
waku/[
waku_core,
@ -14,7 +15,6 @@ import
waku_core/topics,
events/message_events,
waku_node,
common/broker/broker_context,
]
const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries
@ -70,20 +70,30 @@ proc getMissingMsgsFromStore(
)
)
proc processIncomingMessageOfInterest(
proc processIncomingMessage(
self: RecvService, pubsubTopic: string, message: WakuMessage
): bool =
## Deduplicate (by hash), store (saves in recently-seen messages) and emit
## the MAPI MessageReceivedEvent for every unique incoming message.
## Returns true if the message was new and the MessageReceivedEvent was properly emitted.
## Return false if the incoming message is from a non-subscribed topic,
## or if the message is a duplicate (recently-seen). Otherwise, save it as
## recently-seen, emit a MessageReceivedEvent, and return true.
if not self.subscriptionManager.isSubscribed(pubsubTopic, message.contentTopic):
trace "skipping message as I am not subscribed",
shard = pubsubTopic, contentTopic = message.contentTopic
return false
let msgHash = computeMessageHash(pubsubTopic, message)
if not self.recentReceivedMsgs.anyIt(it.msgHash == msgHash):
let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp)
self.recentReceivedMsgs.add(rxMsg)
MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message)
return true
return false
if self.recentReceivedMsgs.anyIt(it.msgHash == msgHash):
trace "skipping duplicate message",
shard = pubsubTopic,
contentTopic = message.contentTopic,
msg_hash = msgHash.to0xHex()
return false
let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp)
self.recentReceivedMsgs.add(rxMsg)
MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message)
return true
proc checkStore*(self: RecvService) {.async.} =
## Checks the store for messages that were not received directly and
@ -113,15 +123,19 @@ proc checkStore*(self: RecvService) {.async.} =
let missedHashes: seq[WakuMessageHash] =
msgHashesInStore.filterIt(not rxMsgHashes.contains(it))
## Now retrieve the missing WakuMessages and deliver them
let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes)
if missingMsgsRet.isOk():
for msgTuple in missingMsgsRet.get():
if self.processIncomingMessageOfInterest(msgTuple.pubsubTopic, msgTuple.msg):
info "recv service store-recovered message",
msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic
else:
error "failed to retrieve missing messages: ", error = $missingMsgsRet.error
if missedHashes.len > 0:
info "missed messages detected, checking store for missed messages",
pubsubTopic = pubsubTopic, missedCount = missedHashes.len
## Now retrieve the missing WakuMessages and deliver them
let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes)
if missingMsgsRet.isOk():
for msgTuple in missingMsgsRet.get():
if self.processIncomingMessage(msgTuple.pubsubTopic, msgTuple.msg):
info "recv service store-recovered message",
msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic
else:
error "failed to retrieve missing messages: ", error = $missingMsgsRet.error
## update next check times
self.startTimeToCheck = self.endTimeToCheck
@ -159,20 +173,13 @@ proc startRecvService*(self: RecvService) =
self.seenMsgListener = MessageSeenEvent.listen(
self.brokerCtx,
proc(event: MessageSeenEvent) {.async: (raises: []).} =
if not self.subscriptionManager.isSubscribed(
event.topic, event.message.contentTopic
):
trace "skipping message as I am not subscribed",
shard = event.topic, contenttopic = event.message.contentTopic
return
discard self.processIncomingMessageOfInterest(event.topic, event.message),
discard self.processIncomingMessage(event.topic, event.message),
).valueOr:
error "Failed to set MessageSeenEvent listener", error = error
quit(QuitFailure)
proc stopRecvService*(self: RecvService) {.async.} =
MessageSeenEvent.dropListener(self.brokerCtx, self.seenMsgListener)
await MessageSeenEvent.dropListener(self.brokerCtx, self.seenMsgListener)
if not self.msgCheckerHandler.isNil():
await self.msgCheckerHandler.cancelAndWait()
self.msgCheckerHandler = nil

View File

@ -1,6 +1,6 @@
import std/[options, times], chronos
import brokers/broker_context
import waku/waku_core, waku/api/types, waku/requests/node_requests
import waku/common/broker/broker_context
type DeliveryState* {.pure.} = enum
Entry

View File

@ -1,11 +1,7 @@
import chronicles, chronos, results
import std/options
import
waku/node/peer_manager,
waku/waku_core,
waku/waku_lightpush/[common, client, rpc],
waku/common/broker/broker_context
import brokers/broker_context
import waku/node/peer_manager, waku/waku_core, waku/waku_lightpush/[common, client, rpc]
import ./[delivery_task, send_processor]

View File

@ -1,8 +1,8 @@
import std/options
import chronos, chronicles
import brokers/broker_context
import waku/[waku_core], waku/waku_lightpush/[common, rpc]
import waku/requests/health_requests
import waku/common/broker/broker_context
import waku/api/types
import ./[delivery_task, send_processor]

View File

@ -1,6 +1,6 @@
import chronos
import brokers/broker_context
import ./delivery_task
import waku/common/broker/broker_context
{.push raises: [].}

View File

@ -3,6 +3,7 @@
import std/[sequtils, tables, options]
import chronos, chronicles, libp2p/utility
import brokers/broker_context
import
./[send_processor, relay_processor, lightpush_processor, delivery_task],
../[subscription_manager],
@ -17,7 +18,6 @@ import
waku_lightpush/client,
waku_lightpush/callbacks,
events/message_events,
common/broker/broker_context,
]
logScope:
@ -225,9 +225,12 @@ proc evaluateAndCleanUp(self: SendService) =
it.state != DeliveryState.FailedToDeliver
)
# remove propagated ephemeral messages as no store check is possible
# remove propagated messages when no store confirmation will follow
self.taskCache.keepItIf(
not (it.isEphemeral() and it.state == DeliveryState.SuccessfullyPropagated)
not (
it.state == DeliveryState.SuccessfullyPropagated and
(it.isEphemeral() or not self.checkStoreForMessages)
)
)
proc trySendMessages(self: SendService) {.async.} =

View File

@ -1,5 +1,7 @@
import std/[sequtils, sets, tables, options, strutils], chronos, chronicles, results
import libp2p/[peerid, peerinfo]
import brokers/broker_context
import
waku/[
waku_core,
@ -10,7 +12,6 @@ import
waku_filter_v2/common as filter_common,
waku_filter_v2/client as filter_client,
waku_filter_v2/protocol as filter_protocol,
common/broker/broker_context,
events/health_events,
events/peer_events,
requests/health_requests,
@ -61,7 +62,16 @@ type SubscriptionManager* = ref object of RootObj
iterator subscribedTopics*(
self: SubscriptionManager
): (PubsubTopic, HashSet[ContentTopic]) =
## Iterate over all subscribed content topics, batched per shard.
## This is guaranteed to return a non-empty `topics` (content topics) list on iteration.
for pubsub, topics in self.contentTopicSubs.pairs:
# We are iterating over subscribed content topics; if we are subscribed to
# a shard but have no subscription (interest) for any content topic in that
# shard, then avoid triggering an iteration that doesn't advance the intent
# to iterate over content topic subscriptions.
if topics.len == 0:
continue
yield (pubsub, topics)
proc edgeFilterPeerCount*(sm: SubscriptionManager, shard: PubsubTopic): int =
@ -521,7 +531,7 @@ proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} =
if not fut.finished:
await fut.cancelAndWait()
WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener)
await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener)
# ---------------------------------------------------------------------------
# SubscriptionManager Lifecycle (calls Edge behavior above)

View File

@ -10,6 +10,9 @@ declarePublicGauge event_loop_load,
"chronos event loop load EWMA by window (1.0 = sustained lag at MaxAcceptedLag)",
labels = ["window"]
declarePublicCounter event_loop_accumulated_lag_secs,
"chronos event loop total accumulated lag in seconds since node start"
type OnLagChange* = proc(lagTooHigh: bool) {.gcsafe, raises: [].}
proc eventLoopMonitorLoop*(onLagChange: OnLagChange = nil) {.async.} =
@ -55,6 +58,8 @@ proc eventLoopMonitorLoop*(onLagChange: OnLagChange = nil) {.async.} =
let lagSecs = lag.nanoseconds.float64 / 1_000_000_000.0
let load = lagSecs / maxAcceptedLagSecs
event_loop_accumulated_lag_secs.inc(lagSecs)
ewma1m = alpha1m * load + (1.0 - alpha1m) * ewma1m
ewma5m = alpha5m * load + (1.0 - alpha5m) * ewma5m
ewma15m = alpha15m * load + (1.0 - alpha15m) * ewma15m

View File

@ -725,8 +725,10 @@ proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} =
if not isNil(hm.eventLoopMonitorFut):
await hm.eventLoopMonitorFut.cancelAndWait()
WakuPeerEvent.dropListener(hm.node.brokerCtx, hm.peerEventListener)
EventShardTopicHealthChange.dropListener(hm.node.brokerCtx, hm.shardHealthListener)
await WakuPeerEvent.dropListener(hm.node.brokerCtx, hm.peerEventListener)
await EventShardTopicHealthChange.dropListener(
hm.node.brokerCtx, hm.shardHealthListener
)
if not isNil(hm.node.wakuRelay) and not isNil(hm.relayObserver):
hm.node.wakuRelay.removeObserver(hm.relayObserver)

View File

@ -16,7 +16,8 @@ import
libp2p/builders,
libp2p/transports/tcptransport,
libp2p/transports/wstransport,
libp2p/utility
libp2p/utility,
brokers/broker_context
import
waku/[
@ -29,7 +30,6 @@ import
waku_rln_relay,
node/waku_node,
node/peer_manager,
common/broker/broker_context,
events/message_events,
]

View File

@ -8,6 +8,9 @@ import
chronicles,
metrics,
libp2p/[multistream, muxers/muxer, nameresolving/nameresolver, peerstore],
brokers/broker_context
import
waku/[
waku_core,
waku_relay,
@ -21,7 +24,6 @@ import
common/enr,
common/callbacks,
common/utils/parse_size_units,
common/broker/broker_context,
node/health_monitor/online_monitor,
],
./peer_store/peer_storage,
@ -107,6 +109,7 @@ type PeerManager* = ref object of RootObj
online: bool ## state managed by online_monitor module
getShards: GetShards
maxConnections: int
activeStoreRequests*: Table[PeerId, int]
#~~~~~~~~~~~~~~~~~~~#
# Helper Functions #
@ -169,6 +172,23 @@ proc addPeer*(
proc getPeer*(pm: PeerManager, peerId: PeerId): RemotePeerInfo =
return pm.switch.peerStore.getPeer(peerId)
proc addActiveStoreRequest*(pm: PeerManager, peerId: PeerId) {.gcsafe.} =
pm.activeStoreRequests.mgetOrPut(peerId, 0).inc()
proc removeActiveStoreRequest*(pm: PeerManager, peerId: PeerId) {.gcsafe.} =
let count = pm.activeStoreRequests.getOrDefault(peerId, 0)
if count == 0:
return
let newCount = count - 1
if newCount <= 0:
pm.activeStoreRequests.del(peerId)
else:
pm.activeStoreRequests[peerId] = newCount
proc hasActiveStoreRequest*(pm: PeerManager, peerId: PeerId): bool {.gcsafe.} =
pm.activeStoreRequests.contains(peerId)
proc loadFromStorage(pm: PeerManager) {.gcsafe.} =
## Load peers from storage, if available
@ -519,6 +539,15 @@ proc connectedPeers*(
return (inPeers, outPeers)
proc evictPeer*(pm: PeerManager, peerId: PeerId) {.async.} =
## Policy-based eviction (relay-peer limit, IP colocation, pruning).
## Skips the disconnect when the peer has an in-flight store request to
## avoid aborting active store requests.
if pm.hasActiveStoreRequest(peerId):
trace "skipping peer eviction: active store request", peerId = peerId
return
await pm.switch.disconnect(peerId)
proc capablePeers*(pm: PeerManager, protocol: string): (seq[PeerId], seq[PeerId]) =
## Returns the PeerIds of peers with an active socket connection.
## If a protocol is specified, it returns peers that have identified
@ -770,11 +799,11 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} =
let inRelayPeers = pm.connectedPeers(WakuRelayCodec)[0]
if inRelayPeers.len > pm.inRelayPeersTarget and
peerStore.hasPeer(peerId, WakuRelayCodec):
info "disconnecting relay peer because reached max num in-relay peers",
info "relay peer limit reached, evicting peer",
peerId = peerId,
inRelayPeers = inRelayPeers.len,
inRelayPeersTarget = pm.inRelayPeersTarget
await pm.switch.disconnect(peerId)
await pm.evictPeer(peerId)
## Apply max ip colocation limit
if (let ip = pm.getPeerIp(peerId); ip.isSome()):
@ -787,7 +816,7 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} =
if pm.colocationLimit != 0 and peersBehindIp.len > pm.colocationLimit:
for peerId in peersBehindIp[0 ..< (peersBehindIp.len - pm.colocationLimit)]:
info "Pruning connection due to ip colocation", peerId = peerId, ip = ip
asyncSpawn(pm.switch.disconnect(peerId))
asyncSpawn(pm.evictPeer(peerId))
peerStore.delete(peerId)
WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventConnected)
@ -1100,7 +1129,7 @@ proc pruneInRelayConns(pm: PeerManager, amount: int) {.async.} =
for p in inRelayPeers[0 ..< connsToPrune]:
trace "Pruning Peer", Peer = $p
asyncSpawn(pm.switch.disconnect(p))
asyncSpawn(pm.evictPeer(p))
proc addExtPeerEventHandler*(
pm: PeerManager, eventHandler: PeerEventHandler, eventKind: PeerEventKind
@ -1214,6 +1243,7 @@ proc new*(
pm.serviceSlots = initTable[string, RemotePeerInfo]()
pm.ipTable = initTable[string, seq[PeerId]]()
pm.activeStoreRequests = initTable[PeerId, int]()
if not storage.isNil():
trace "found persistent peer storage"

View File

@ -2,8 +2,7 @@
import chronicles, chronos, metrics, metrics/chronos_httpserver
import
../waku_rln_relay/protocol_metrics as rln_metrics,
../utils/collector,
waku/[net/auto_port, waku_rln_relay/protocol_metrics as rln_metrics, utils/collector],
./peer_manager,
./waku_node
@ -57,27 +56,36 @@ proc startMetricsLog*() =
discard setTimer(Moment.fromNow(LogInterval), logMetrics)
type StartedMetricsServer* = tuple[server: MetricsHttpServerRef, port: Port]
proc startMetricsServer(
serverIp: IpAddress, serverPort: Port
): Future[Result[MetricsHttpServerRef, string]] {.async.} =
info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $serverPort
): Future[Result[StartedMetricsServer, string]] {.async.} =
proc attempt(
port: Port
): Future[Result[StartedMetricsServer, string]] {.async: (raises: []).} =
info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $port
let server = MetricsHttpServerRef.new($serverIp, serverPort).valueOr:
return err("metrics HTTP server start failed: " & $error)
let server = MetricsHttpServerRef.new($serverIp, port).valueOr:
return err("fail to start service metrics server, attempt:" & $error)
try:
await server.start()
except CatchableError:
return err("metrics HTTP server start failed: " & getCurrentExceptionMsg())
try:
await server.start()
except CatchableError:
return
err("exception while startMetricsServer, attempt: " & getCurrentExceptionMsg())
info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $serverPort
return ok(server)
info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $port
return ok((server: server, port: port))
let started = (await tryWithAutoPort[StartedMetricsServer](serverPort, attempt)).valueOr:
return err("metrics HTTP server start failed: " & error)
return ok(started)
proc startMetricsServerAndLogging*(
conf: MetricsServerConf, portsShift: uint16
): Future[Result[MetricsHttpServerRef, string]] {.async.} =
var metricsServer: MetricsHttpServerRef
metricsServer = (
): Future[Result[StartedMetricsServer, string]] {.async.} =
let started = (
await (
startMetricsServer(conf.httpAddress, Port(conf.httpPort.uint16 + portsShift))
)
@ -87,4 +95,4 @@ proc startMetricsServerAndLogging*(
if conf.logging:
startMetricsLog()
return ok(metricsServer)
return ok(started)

View File

@ -24,7 +24,9 @@ import
libp2p/utility,
libp2p/utils/offsettedseq,
libp2p/protocols/mix,
libp2p/protocols/mix/mix_protocol
libp2p/protocols/mix/mix_protocol,
brokers/broker_context,
brokers/request_broker
import
waku/[
@ -53,8 +55,6 @@ import
common/rate_limit/setting,
common/callbacks,
common/nimchronos,
common/broker/broker_context,
common/broker/request_broker,
waku_mix,
requests/node_requests,
requests/health_requests,
@ -62,7 +62,7 @@ import
events/message_events,
],
waku/discovery/waku_kademlia,
./net_config,
waku/net/[bound_ports, net_config],
./peer_manager,
./health_monitor/health_status,
./health_monitor/topic_health
@ -140,6 +140,7 @@ type
wakuMix*: WakuMix
kademliaDiscoveryLoop*: Future[void]
wakuKademlia*: WakuKademlia
ports*: BoundPorts
proc deduceRelayShard(
node: WakuNode,
@ -224,6 +225,7 @@ proc new*(
announcedAddresses: netConfig.announcedAddresses,
topicSubscriptionQueue: queue,
rateLimitSettings: rateLimitSettings,
ports: BoundPorts.init(),
)
peerManager.setShardGetter(node.getShardsGetter(@[]))

View File

@ -0,0 +1,161 @@
## Cross-thread broker declarations for the persistency library.
##
## One EventBroker (writes, fire-and-forget) and five RequestBrokers (reads
## + acked delete). All in multi-thread (mt) mode: the listener / provider runs on the
## job's storage thread; callers on any thread reach it via the shared
## BrokerContext owned by the Job.
##
## ## Error type, important
##
## nim-brokers' RequestBroker macro hard-codes the response shape as
## `Future[Result[ResponseType, string]]` — the error channel is `string`,
## not our `PersistencyError`. We honour the broker contract here and lift
## back to `PersistencyError` at the public facade (persistency.nim). The
## convention for the broker-level string is `"<kind>: <msg>"` so the
## facade can reconstruct the `PersistencyErrorKind`.
##
## ## Response shapes
##
## The five Kv* types are *response* objects (the value the provider
## returns). Per-request inputs sit on the `signature` proc parameters.
{.push raises: [].}
import std/[options, strutils]
import chronos, results
import brokers/[event_broker, request_broker, broker_context]
import brokers/internal/mt_codec
import ./types
export broker_context
# ── mt codec overloads for non-POD library types ────────────────────────
#
# brokers 2.0.0's mtMarshalValue / mtUnmarshalValue handle scalars, enums,
# strings, seqs, arrays, and plain object/tuple recursion -- but they do
# not see through `distinct seq[byte]`, nor do they know how to dispatch
# a variant (case) object. We provide explicit overloads for the types
# that appear in our broker payloads.
proc mtMarshalValue*(
buf: ptr UncheckedArray[byte], cap: int, value: Key, pos: var int
): bool {.gcsafe.} =
## Encode a Key as the raw seq[byte] it wraps.
mtMarshalValue(buf, cap, bytes(value), pos)
proc mtUnmarshalValue*(
buf: ptr UncheckedArray[byte], len: int, value: var Key, pos: var int
): bool {.gcsafe.} =
var s: seq[byte]
if not mtUnmarshalValue(buf, len, s, pos):
return false
value = Key(s)
return true
proc mtMarshalValue*(
buf: ptr UncheckedArray[byte], cap: int, value: TxOp, pos: var int
): bool {.gcsafe.} =
## TxOp is a case object: write the discriminator, then only the
## fields that belong to the active branch.
if not mtMarshalValue(buf, cap, value.category, pos):
return false
if not mtMarshalValue(buf, cap, value.key, pos):
return false
let kind = uint8(ord(value.kind))
if not mtMarshalValue(buf, cap, kind, pos):
return false
case value.kind
of txPut:
if not mtMarshalValue(buf, cap, value.payload, pos):
return false
of txDelete:
discard
return true
proc mtUnmarshalValue*(
buf: ptr UncheckedArray[byte], len: int, value: var TxOp, pos: var int
): bool {.gcsafe.} =
var
category: string
key: Key
kindByte: uint8
if not mtUnmarshalValue(buf, len, category, pos):
return false
if not mtUnmarshalValue(buf, len, key, pos):
return false
if not mtUnmarshalValue(buf, len, kindByte, pos):
return false
case TxOpKind(kindByte)
of txPut:
var payload: seq[byte]
if not mtUnmarshalValue(buf, len, payload, pos):
return false
value = TxOp(category: category, key: key, kind: txPut, payload: payload)
of txDelete:
value = TxOp(category: category, key: key, kind: txDelete)
return true
EventBroker(mt):
type PersistEvent* = object
ops*: seq[TxOp]
RequestBroker(mt):
type KvGet* = object
value*: Option[seq[byte]]
proc signature*(category: string, key: Key): Future[Result[KvGet, string]] {.async.}
RequestBroker(mt):
type KvExists* = object
value*: bool
proc signature*(
category: string, key: Key
): Future[Result[KvExists, string]] {.async.}
RequestBroker(mt):
type KvScan* = object
rows*: seq[KvRow]
proc signature*(
category: string, range: KeyRange, reverse: bool
): Future[Result[KvScan, string]] {.async.}
RequestBroker(mt):
type KvCount* = object
n*: int
proc signature*(
category: string, range: KeyRange
): Future[Result[KvCount, string]] {.async.}
RequestBroker(mt):
type KvDelete* = object
existed*: bool
proc signature*(
category: string, key: Key
): Future[Result[KvDelete, string]] {.async.}
# ── string<->PersistencyError boundary helpers ──────────────────────────
const ErrSep = ": "
proc encodeErr*(e: PersistencyError): string =
## Encode a PersistencyError into the broker's string channel. The facade
## decodes via `decodeErr`.
$e.kind & ErrSep & e.msg
proc decodeErr*(s: string): PersistencyError =
## Inverse of encodeErr. Falls back to peBackend if the prefix is missing.
let idx = s.find(ErrSep)
if idx < 0:
return persistencyErr(peBackend, s)
let head = s[0 ..< idx]
let tail = s[idx + ErrSep.len .. ^1]
for k in PersistencyErrorKind:
if $k == head:
return persistencyErr(k, tail)
persistencyErr(peBackend, s)
{.pop.}

View File

@ -0,0 +1,247 @@
## Synchronous SQLite backend for the persistency library.
##
## Plain procs against a SqliteDatabase connection. Phase 3 wraps these in
## per-job storage threads driven by brokers; phase 2 verifies the SQL
## itself against an in-memory database.
import std/options
import results, sqlite3_abi
import ../common/databases/[common, db_sqlite]
import ./[types, schema]
type
KvBackend* = ref object
db*: SqliteDatabase
putStmt: SqliteStmt[(seq[byte], seq[byte], seq[byte]), void]
deleteStmt: SqliteStmt[(seq[byte], seq[byte]), void]
RowHandler = proc(s: ptr sqlite3_stmt) {.gcsafe, raises: [].}
proc toErr(msg: string): PersistencyError {.inline.} =
persistencyErr(peBackend, msg)
proc catBytes(category: string): seq[byte] =
var buf = newSeq[byte](category.len)
for i, c in category:
buf[i] = byte(c)
return buf
proc keyBytes(key: Key): seq[byte] {.inline.} =
bytes(key)
proc readBlob(s: ptr sqlite3_stmt, col: cint): seq[byte] =
let n = sqlite3_column_bytes(s, col)
var buf = newSeq[byte](n)
if n > 0:
let src = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, col))
for i in 0 ..< n:
buf[i] = src[i]
return buf
proc bindBlob(s: ptr sqlite3_stmt, n: cint, val: seq[byte]): cint =
if val.len > 0:
sqlite3_bind_blob(s, n, unsafeAddr val[0], val.len.cint, SQLITE_TRANSIENT)
else:
sqlite3_bind_blob(s, n, nil, 0.cint, SQLITE_TRANSIENT)
proc runRead(
db: SqliteDatabase, sql: string, params: openArray[seq[byte]], onRow: RowHandler
): Result[void, PersistencyError] =
var s: ptr sqlite3_stmt
let rc = sqlite3_prepare_v2(db.env, sql.cstring, sql.len.cint, addr s, nil)
if rc != SQLITE_OK:
return err(toErr("prepare: " & $sqlite3_errstr(rc)))
defer:
discard sqlite3_finalize(s)
for i, p in params:
let bc = bindBlob(s, cint(i + 1), p)
if bc != SQLITE_OK:
return err(toErr("bind: " & $sqlite3_errstr(bc)))
while true:
let v = sqlite3_step(s)
case v
of SQLITE_ROW:
onRow(s)
of SQLITE_DONE:
break
else:
return err(toErr("step: " & $sqlite3_errstr(v)))
return ok()
proc prepareStatements(b: KvBackend): DatabaseResult[void] =
b.putStmt = ?b.db.prepareStmt(
"INSERT OR REPLACE INTO kv(category, key, payload) VALUES (?, ?, ?);",
(seq[byte], seq[byte], seq[byte]),
void,
)
b.deleteStmt = ?b.db.prepareStmt(
"DELETE FROM kv WHERE category = ? AND key = ?;", (seq[byte], seq[byte]), void
)
return ok()
proc openBackend*(path: string): Result[KvBackend, PersistencyError] =
let dbRes = SqliteDatabase.new(path)
if dbRes.isErr:
return err(toErr("open " & path & " failed: " & dbRes.error))
let db = dbRes.get()
applyPragmas(db).isOkOr:
return err(toErr(error))
ensureSchema(db).isOkOr:
return err(toErr(error))
let b = KvBackend(db: db)
prepareStatements(b).isOkOr:
return err(toErr(error))
return ok(b)
proc openBackendInMemory*(): Result[KvBackend, PersistencyError] =
## Convenience for tests.
let dbRes = SqliteDatabase.new(":memory:")
if dbRes.isErr:
return err(toErr("open :memory: failed: " & dbRes.error))
let db = dbRes.get()
applyPragmas(db).isOkOr:
return err(toErr(error))
ensureSchema(db).isOkOr:
return err(toErr(error))
let b = KvBackend(db: db)
prepareStatements(b).isOkOr:
return err(toErr(error))
return ok(b)
proc close*(b: KvBackend) =
if b.db != nil:
dispose(b.putStmt)
dispose(b.deleteStmt)
b.db.close()
b.db = nil
proc applyOne(b: KvBackend, op: TxOp): Result[void, PersistencyError] =
case op.kind
of txPut:
let r = b.putStmt.exec((catBytes(op.category), keyBytes(op.key), op.payload))
if r.isErr:
return err(toErr("put failed: " & r.error))
of txDelete:
let r = b.deleteStmt.exec((catBytes(op.category), keyBytes(op.key)))
if r.isErr:
return err(toErr("delete failed: " & r.error))
return ok()
proc execSql(b: KvBackend, sql: string): Result[void, PersistencyError] =
let r = b.db.query(sql, NoopRowHandler)
if r.isErr:
return err(toErr(sql & ": " & r.error))
return ok()
proc applyOps*(b: KvBackend, ops: openArray[TxOp]): Result[void, PersistencyError] =
## Single op = auto-commit. Multiple ops = BEGIN IMMEDIATE / COMMIT, with
## ROLLBACK on first failure. This is the single source of truth for write
## SQL — Phase 3's PersistEvent listener calls straight into here.
if ops.len == 0:
return ok()
if ops.len == 1:
return b.applyOne(ops[0])
?b.execSql("BEGIN IMMEDIATE;")
for op in ops:
let r = b.applyOne(op)
if r.isErr:
discard b.execSql("ROLLBACK;")
return r
?b.execSql("COMMIT;")
return ok()
proc getOne*(
b: KvBackend, category: string, key: Key
): Result[Option[seq[byte]], PersistencyError] =
var found: Option[seq[byte]] = none(seq[byte])
proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} =
found = some(readBlob(rs, 0.cint))
?b.db.runRead(
"SELECT payload FROM kv WHERE category = ? AND key = ? LIMIT 1;",
[catBytes(category), keyBytes(key)],
onRow,
)
return ok(found)
proc existsOne*(
b: KvBackend, category: string, key: Key
): Result[bool, PersistencyError] =
var present = false
proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} =
present = true
?b.db.runRead(
"SELECT 1 FROM kv WHERE category = ? AND key = ? LIMIT 1;",
[catBytes(category), keyBytes(key)],
onRow,
)
return ok(present)
proc deleteOne*(
b: KvBackend, category: string, key: Key
): Result[bool, PersistencyError] =
## Returns true if a row was actually removed.
let existed = ?b.existsOne(category, key)
if not existed:
return ok(false)
let r = b.deleteStmt.exec((catBytes(category), keyBytes(key)))
if r.isErr:
return err(toErr("delete: " & r.error))
return ok(true)
proc scanRange*(
b: KvBackend, category: string, range: KeyRange, reverse = false
): Result[seq[KvRow], PersistencyError] =
let openEnded = bytes(range.stop).len == 0
let direction = if reverse: "DESC" else: "ASC"
let sql =
if openEnded:
"SELECT key, payload FROM kv WHERE category = ? AND key >= ? ORDER BY key " &
direction & ";"
else:
"SELECT key, payload FROM kv WHERE category = ? AND key >= ? AND key < ? ORDER BY key " &
direction & ";"
var rows: seq[KvRow] = @[]
proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} =
let k = readBlob(rs, 0.cint)
let p = readBlob(rs, 1.cint)
rows.add((rawKey(k), p))
if openEnded:
?b.db.runRead(sql, [catBytes(category), keyBytes(range.start)], onRow)
else:
?b.db.runRead(
sql, [catBytes(category), keyBytes(range.start), keyBytes(range.stop)], onRow
)
return ok(rows)
proc countRange*(
b: KvBackend, category: string, range: KeyRange
): Result[int, PersistencyError] =
let openEnded = bytes(range.stop).len == 0
let sql =
if openEnded:
"SELECT COUNT(*) FROM kv WHERE category = ? AND key >= ?;"
else:
"SELECT COUNT(*) FROM kv WHERE category = ? AND key >= ? AND key < ?;"
var n: int64 = 0
proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} =
n = sqlite3_column_int64(rs, 0.cint)
if openEnded:
?b.db.runRead(sql, [catBytes(category), keyBytes(range.start)], onRow)
else:
?b.db.runRead(
sql, [catBytes(category), keyBytes(range.start), keyBytes(range.stop)], onRow
)
return ok(int(n))

View File

@ -0,0 +1,271 @@
## Internal per-job storage thread.
##
## Exposes two operations to ``persistency.nim``:
## * ``startStorageThread(ctx, dbPath)`` — spawn one worker, block until
## it signals ready (or error). Returns a ``JobRuntime``.
## * ``stopStorageThread(rt)`` — signal shutdown, join, free.
##
## The worker:
## 1. installs the supplied BrokerContext on its threadvar
## 2. opens the SQLite backend (creating the file + schema if absent)
## 3. registers the PersistEvent listener and the 5 RequestBroker
## providers under that context
## 4. runs the chronos event loop until shutdown is signalled
## 5. clears providers + listeners, closes the backend
##
## The arg struct lives in shared memory (``allocShared0``). The dbPath is
## carried as a shared cstring buffer rather than a Nim string to avoid
## refc ref-count traffic across threads. The arg is freed by
## ``stopStorageThread`` after ``joinThread`` returns.
import std/[options, os]
import std/atomics # std/concurrency/atomics is the same module in Nim 2.2
import chronos, chronicles, results
import brokers/[event_broker, request_broker, broker_context]
import ./[types, backend_comm, backend_sqlite]
export broker_context, backend_comm
logScope:
topics = "persistency thread"
type
ReadyState {.pure.} = enum
Pending = 0
Ready = 1
Error = 2
StorageThreadArg = object
ctx: BrokerContext
dbPath: cstring ## allocShared0'd; freed in closeJob
dbPathLen: int ## bytes including the trailing NUL
shutdownFlag: Atomic[int]
readyFlag: Atomic[int] ## values from ReadyState
errBuf: array[256, char] ## last error message, NUL-terminated
StorageThread = Thread[ptr StorageThreadArg]
# ── arg helpers ─────────────────────────────────────────────────────────
proc allocArg(ctx: BrokerContext, dbPath: string): ptr StorageThreadArg =
let arg = cast[ptr StorageThreadArg](allocShared0(sizeof(StorageThreadArg)))
arg.ctx = ctx
arg.dbPathLen = dbPath.len + 1
arg.dbPath = cast[cstring](allocShared0(arg.dbPathLen))
if dbPath.len > 0:
copyMem(arg.dbPath, unsafeAddr dbPath[0], dbPath.len)
return arg
proc freeArg(a: ptr StorageThreadArg) =
if a.isNil():
return
if a.dbPath != nil:
deallocShared(a.dbPath)
deallocShared(a)
proc recordErr(a: ptr StorageThreadArg, msg: string) =
let n = min(msg.len, a.errBuf.len - 1)
for i in 0 ..< n:
a.errBuf[i] = msg[i]
a.errBuf[n] = '\0'
a.readyFlag.store(int(ReadyState.Error), moRelease)
proc errMsg(a: ptr StorageThreadArg): string =
$cast[cstring](a.errBuf[0].addr)
# ── provider closures ───────────────────────────────────────────────────
proc encode(e: PersistencyError): string =
encodeErr(e)
template unwrapErr(r: untyped): string =
## Disambiguates Result's `error` accessor from chronicles' `error` macro
## by binding through an explicitly-typed local before stringifying.
block:
let pe: PersistencyError = r.error()
encode(pe)
proc registerProviders(backend: KvBackend, ctx: BrokerContext): Result[void, string] =
## Wires the 5 RequestBroker providers + the PersistEvent listener.
## All closures capture `backend` by reference (it lives for the entire
## thread lifetime).
proc onGet(category: string, key: Key): Future[Result[KvGet, string]] {.async.} =
let r = backend.getOne(category, key)
if r.isErr:
return err(unwrapErr(r))
return ok(KvGet(value: r.get()))
proc onExists(
category: string, key: Key
): Future[Result[KvExists, string]] {.async.} =
let r = backend.existsOne(category, key)
if r.isErr:
return err(unwrapErr(r))
return ok(KvExists(value: r.get()))
proc onScan(
category: string, range: KeyRange, reverse: bool
): Future[Result[KvScan, string]] {.async.} =
let r = backend.scanRange(category, range, reverse)
if r.isErr:
return err(unwrapErr(r))
return ok(KvScan(rows: r.get()))
proc onCount(
category: string, range: KeyRange
): Future[Result[KvCount, string]] {.async.} =
let r = backend.countRange(category, range)
if r.isErr:
return err(unwrapErr(r))
return ok(KvCount(n: r.get()))
proc onDelete(
category: string, key: Key
): Future[Result[KvDelete, string]] {.async.} =
let r = backend.deleteOne(category, key)
if r.isErr:
return err(unwrapErr(r))
return ok(KvDelete(existed: r.get()))
# PersistEvent listener — fire-and-forget; we log on backend failure
# because the caller has no return channel.
proc onPersist(ev: PersistEvent): Future[void] {.async: (raises: []).} =
let r = backend.applyOps(ev.ops)
if r.isErr:
let pe: PersistencyError = r.error()
error "PersistEvent applyOps failed",
error = pe.msg, kind = $pe.kind, opCount = ev.ops.len
KvGet.setProvider(ctx, onGet).isOkOr:
return err("KvGet.setProvider: " & error)
let existsRes = KvExists.setProvider(ctx, onExists)
if existsRes.isErr:
return err("KvExists.setProvider: " & existsRes.error())
let scanRes = KvScan.setProvider(ctx, onScan)
if scanRes.isErr:
return err("KvScan.setProvider: " & scanRes.error())
let countRes = KvCount.setProvider(ctx, onCount)
if countRes.isErr:
return err("KvCount.setProvider: " & countRes.error())
let delRes = KvDelete.setProvider(ctx, onDelete)
if delRes.isErr:
return err("KvDelete.setProvider: " & delRes.error())
let listenRes = PersistEvent.listen(ctx, onPersist)
if listenRes.isErr:
return err("PersistEvent.listen: " & listenRes.error())
return ok()
proc clearProviders(ctx: BrokerContext) =
KvGet.clearProvider(ctx)
KvExists.clearProvider(ctx)
KvScan.clearProvider(ctx)
KvCount.clearProvider(ctx)
KvDelete.clearProvider(ctx)
PersistEvent.dropAllListeners(ctx)
# ── thread proc ─────────────────────────────────────────────────────────
proc storageThreadMain(arg: ptr StorageThreadArg) {.thread.} =
## Worker thread entrypoint. Errors during setup are surfaced via
## arg.errBuf + readyFlag=ReadyState.Error; the spawning thread checks both.
setThreadBrokerContext(arg.ctx)
let path = $arg.dbPath
let backendRes =
try:
openBackend(path)
except CatchableError as e:
arg.recordErr("openBackend raised: " & e.msg)
return
if backendRes.isErr:
arg.recordErr("openBackend: " & backendRes.error.msg)
return
let backend = backendRes.get()
let regRes =
try:
registerProviders(backend, arg.ctx)
except CatchableError as e:
backend.close()
arg.recordErr("registerProviders raised: " & e.msg)
return
if regRes.isErr:
backend.close()
arg.recordErr(regRes.error)
return
arg.readyFlag.store(int(ReadyState.Ready), moRelease)
proc awaitShutdown() {.async.} =
while arg.shutdownFlag.load(moAcquire) != 1:
try:
await sleepAsync(milliseconds(10))
except CatchableError:
discard
try:
waitFor awaitShutdown()
except CatchableError as e:
error "storage thread loop crashed", err = e.msg
clearProviders(arg.ctx)
backend.close()
# ── lifecycle ───────────────────────────────────────────────────────────
type JobRuntime* = ref object
## Opaque per-job runtime owned by `persistency.nim`. Holds the typed
## Thread handle + shared arg pointer so closeJob can shut the worker
## down. Created by `startStorageThread` and torn down by
## `stopStorageThread`.
arg*: ptr StorageThreadArg
thread*: StorageThread
proc startStorageThread*(
ctx: BrokerContext, dbPath: string
): Result[JobRuntime, PersistencyError] =
## Spawn a storage worker for one job. Blocks until the worker either
## signals ready (returns the runtime) or signals error (joins, frees,
## returns peBackend with the worker's error message).
let arg = allocArg(ctx, dbPath)
arg.shutdownFlag.store(0, moRelease)
arg.readyFlag.store(int(ReadyState.Pending), moRelease)
var rt = JobRuntime(arg: arg)
try:
createThread(rt.thread, storageThreadMain, arg)
except ResourceExhaustedError as e:
freeArg(arg)
return err(persistencyErr(peBackend, "createThread: " & e.msg))
# Spin-wait for ready or error. The thread does its setup synchronously
# before signaling, so this is bounded by SQLite open time.
while true:
let s = arg.readyFlag.load(moAcquire)
if s == int(ReadyState.Ready):
return ok(rt)
if s == int(ReadyState.Error):
let msg = errMsg(arg)
joinThread(rt.thread)
freeArg(arg)
return err(persistencyErr(peBackend, msg))
sleep(1)
proc stopStorageThread*(rt: JobRuntime) =
## Signal shutdown, join the worker, free the shared arg. Idempotent in
## the sense that it tolerates a nil arg (already stopped).
if rt == nil or rt.arg == nil:
return
rt.arg.shutdownFlag.store(1, moRelease)
joinThread(rt.thread)
freeArg(rt.arg)
rt.arg = nil

Some files were not shown because too many files have changed in this diff Show More