mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-06-04 13:09:32 +00:00
Merge branch 'master' into dummy_pr_ci_verfication
This commit is contained in:
commit
cb84ec1074
2
.github/ISSUE_TEMPLATE/prepare_release.md
vendored
2
.github/ISSUE_TEMPLATE/prepare_release.md
vendored
@ -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.
|
||||
|
||||
|
||||
28
.github/workflows/release-assets.yml
vendored
28
.github/workflows/release-assets.yml
vendored
@ -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
49
.github/workflows/version-check.yml
vendored
Normal 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
1
.gitignore
vendored
@ -85,3 +85,4 @@ nimble.paths
|
||||
nimbledeps
|
||||
|
||||
**/anvil_state/state-deployed-contracts-mint-and-approved.json
|
||||
.gitnexus
|
||||
|
||||
42
AGENTS.md
42
AGENTS.md
@ -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 -->
|
||||
|
||||
@ -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
|
||||
|
||||
2
Makefile
2
Makefile
@ -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
|
||||
|
||||
@ -13,7 +13,8 @@ import
|
||||
chronos,
|
||||
eth/keys,
|
||||
bearssl,
|
||||
stew/[byteutils, results],
|
||||
stew/[byteutils],
|
||||
results,
|
||||
metrics,
|
||||
metrics/chronos_httpserver
|
||||
import
|
||||
|
||||
@ -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]))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
6
flake.lock
generated
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
53
flake.nix
53
flake.nix
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
52
nimble.lock
52
nimble.lock
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
21
nix/deps.nix
21
nix/deps.nix
@ -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";
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -85,3 +85,6 @@ import ./api/test_all
|
||||
|
||||
# Waku tools tests
|
||||
import ./tools/test_all
|
||||
|
||||
# Persistency library tests
|
||||
import ./persistency/test_all
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
9
tests/persistency/test_all.nim
Normal file
9
tests/persistency/test_all.nim
Normal 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
|
||||
195
tests/persistency/test_backend.nim
Normal file
195
tests/persistency/test_backend.nim
Normal 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
|
||||
154
tests/persistency/test_encoding.nim
Normal file
154
tests/persistency/test_encoding.nim
Normal 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)
|
||||
196
tests/persistency/test_facade.nim
Normal file
196
tests/persistency/test_facade.nim
Normal 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
|
||||
135
tests/persistency/test_keys.nim
Normal file
135
tests/persistency/test_keys.nim
Normal 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
|
||||
302
tests/persistency/test_lifecycle.nim
Normal file
302
tests/persistency/test_lifecycle.nim
Normal 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
|
||||
79
tests/persistency/test_singleton.nim
Normal file
79
tests/persistency/test_singleton.nim
Normal 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
|
||||
184
tests/persistency/test_string_lookup.nim
Normal file
184
tests/persistency/test_string_lookup.nim
Normal 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
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{.used.}
|
||||
|
||||
import
|
||||
std/options,
|
||||
testutils/unittests,
|
||||
presto,
|
||||
presto/client as presto_client,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
2
vendor/zerokit
vendored
@ -1 +1 @@
|
||||
Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b
|
||||
Subproject commit 5e64cb8822bee65eed6cf459f95ae72b80c6ba63
|
||||
16
waku.nimble
16
waku.nimble
@ -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"
|
||||
|
||||
|
||||
@ -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.}
|
||||
@ -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
|
||||
@ -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,
|
||||
)
|
||||
@ -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
|
||||
@ -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))
|
||||
@ -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] =
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import waku/common/broker/event_broker
|
||||
import brokers/event_broker
|
||||
import libp2p/switch
|
||||
|
||||
type WakuPeerEventKind* {.pure.} = enum
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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())
|
||||
),
|
||||
|
||||
@ -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.} =
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -152,6 +152,8 @@ type WakuConf* {.requiresInit.} = ref object
|
||||
|
||||
p2pReliability*: bool
|
||||
|
||||
localStoragePath*: string
|
||||
|
||||
proc logConf*(conf: WakuConf) =
|
||||
info "Configuration: Enabled protocols",
|
||||
relay = conf.relay,
|
||||
|
||||
@ -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
48
waku/net/auto_port.nim
Normal 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
20
waku/net/bound_ports.nim
Normal 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)
|
||||
@ -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(
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import chronos
|
||||
import brokers/broker_context
|
||||
import ./delivery_task
|
||||
import waku/common/broker/broker_context
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
|
||||
@ -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.} =
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
]
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(@[]))
|
||||
|
||||
161
waku/persistency/backend_comm.nim
Normal file
161
waku/persistency/backend_comm.nim
Normal 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.}
|
||||
247
waku/persistency/backend_sqlite.nim
Normal file
247
waku/persistency/backend_sqlite.nim
Normal 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))
|
||||
271
waku/persistency/backend_thread.nim
Normal file
271
waku/persistency/backend_thread.nim
Normal 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
Loading…
x
Reference in New Issue
Block a user