[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[gnunet-scheme] 03/03: concurrency/lost-and-found: New module.
From: |
gnunet |
Subject: |
[gnunet-scheme] 03/03: concurrency/lost-and-found: New module. |
Date: |
Thu, 10 Feb 2022 18:47:54 +0100 |
This is an automated email from the git hooks/post-receive script.
maxime-devos pushed a commit to branch master
in repository gnunet-scheme.
commit 7f6e421e9085ffc72815cde41404f484dd803ab6
Author: Maxime Devos <maximedevos@telenet.be>
AuthorDate: Thu Feb 10 17:31:38 2022 +0000
concurrency/lost-and-found: New module.
It will be used by (gnu gnunet dht client), and later (gnu gnunet
cadet client).
* gnu/gnunet/concurrency/lost-and-found.scm: New module.
* tests/lost-and-found.scm: New test.
* Makefile.am (modules, SCM_TESTS): Register new files.
---
Makefile.am | 2 +
gnu/gnunet/concurrency/lost-and-found.scm | 237 +++++++++++++++++++++++
tests/lost-and-found.scm | 312 ++++++++++++++++++++++++++++++
3 files changed, 551 insertions(+)
diff --git a/Makefile.am b/Makefile.am
index 00120f2..b42b6cc 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -47,6 +47,7 @@ modules = \
\
gnu/gnunet/concurrency/update.scm \
gnu/gnunet/concurrency/repeated-condition.scm \
+ gnu/gnunet/concurrency/lost-and-found.scm \
\
gnu/gnunet/mq/envelope.scm \
gnu/gnunet/mq/error-reporting.scm \
@@ -194,6 +195,7 @@ SCM_TESTS = \
tests/crypto.scm \
tests/distributed-hash-table.scm \
tests/form.scm \
+ tests/lost-and-found.scm \
tests/netstruct.scm \
tests/time.scm \
tests/tokeniser.scm
diff --git a/gnu/gnunet/concurrency/lost-and-found.scm
b/gnu/gnunet/concurrency/lost-and-found.scm
new file mode 100644
index 0000000..8975240
--- /dev/null
+++ b/gnu/gnunet/concurrency/lost-and-found.scm
@@ -0,0 +1,237 @@
+;; This file is part of Scheme-GNUnet
+;; Copyright © 2022 GNUnet e.V.
+;;
+;; Scheme-GNUnet is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU Affero General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; Scheme-GNUnet is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; Affero General Public License for more details.
+;;
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+;;
+;; SPDX-License-Identifier: AGPL-3.0-or-later
+
+;; Author: Maxime Devos
+(define-library (gnu gnunet concurrency lost-and-found)
+ (export make-lost-and-found lost-and-found? collect-lost-and-found-operation
+ make-losable <losable> losable?
+ ;; exported for tests
+ (rename (add-found! #{ add-found!}#)))
+ (import (only (rnrs base)
+ begin let define lambda quote if cond eq? assert cons list)
+ (only (rnrs control)
+ when unless)
+ (only (rnrs records syntactic)
+ define-record-type)
+ (only (guile)
+ make-guardian add-hook! after-gc-hook object-address)
+ (only (ice-9 format)
+ format)
+ (only (srfi srfi-9 gnu)
+ set-record-type-printer!)
+ (only (ice-9 atomic)
+ make-atomic-box atomic-box-ref)
+ (only (fibers conditions)
+ make-condition condition? signal-condition! wait-operation)
+ (only (fibers operations)
+ wrap-operation make-base-operation)
+ ;; TODO: move elsewhere
+ (only (gnu gnunet mq envelope)
+ %%bind-atomic-boxen))
+ (begin
+ (define-record-type (<lost-and-found> make-lost-and-found lost-and-found?)
+ ;; Atomic box of [condition | (found found* ...)].
+ ;; When there is nothing found, the condition is unsignalled.
+ ;;
+ ;; To register something lost, it is added to the list (if any),
+ ;; otherwise the condition is replaced by the lost object, then
+ ;; the condition is signalled.
+ (fields (immutable contents-box lost-and-found-contents-box))
+ (protocol (lambda (%make)
+ (lambda ()
+ (%make (make-atomic-box (make-condition)))))))
+
+ (set-record-type-printer!
+ <lost-and-found>
+ (lambda (record port)
+ (format port "#<lost-and-found ~x ~a>"
+ (object-address record)
+ (if (condition?
+ (atomic-box-ref (lost-and-found-contents-box record)))
+ "empty"
+ "non-empty"))))
+
+ ;; TODO: concurrency this operation, not reusable
+ (define (collect-lost-and-found-operation lost-and-found)
+ "Make an operation that will complete when something lost has been
+found and return the newly found objects as a list. If this operation is
+performed multiple times concurrently on the same lost and found, spurious
+wakeups where the empty list is returned are possible."
+ (%%bind-atomic-boxen
+ ((value (lost-and-found-contents-box lost-and-found) swap!))
+ (let ((old value)
+ (new-condition (make-condition)))
+ (define (loop old)
+ ;; The mutation replacing 'old' by 'value' is detected by
+ ;; the tests "new lost between making the operation and performing
+ ;; it".
+ (define new-old (swap! old new-condition))
+ ;; If a condition, a concurrent
+ ;; 'collect-lost-and-found-operation' has took the found
+ ;; objects, return a spurious empty list.
+ ;;
+ ;; The mutations ‘inverse the condition’, ‘remove this clause’,
+ ;; and ‘return new-old’ are detected by the test "concurrent
+ ;; collecting (light)".
+ ;;
+ ;; The mutation ‘replace ‘new-old’ by ‘old’ or ‘value’’ is detected
+ ;; by "new lost between making the operation and performing it (2)".
+ ;;
+ ;; TODO: detect switching the first two clauses.
+ (cond ((condition? new-old) '())
+ ;; eq? and not a condition --> succesfully replaced a
+ ;; list of found objects with 'new-condition', return
+ ;; the list.
+ ;;
+ ;; The mutations ‘remove this clause’, ‘always return the
+ ;; empty list’, ‘inverse the condition’, ‘replace new-old or
+ ;; old by value’ are detected by the test "unreachable + gc ->
+ ;; moved into lost and found".
+ ((eq? old new-old) old)
+ ;; not eq? --> a race happened, retry
+ ;;
+ ;; The mutations ‘removing this clause’,
+ ;; ‘returning the empty list’ and ‘calling loop twice’ are
+ ;; detected by tests "new lost between making the operation and
+ ;; performing it".
+ (#true (loop new-old))))
+ ;; The mutation ‘use value instead of old’ is detected ‘losing and
+ ;; collecting concurrently’ (somewhat irreproducible).
+ (if (condition? old)
+ (wrap-operation
+ ;; The mutation ‘don't wait for anything’ is detected by
+ ;; the test "block while nothing to collect".
+ ;; The mutation ‘use value instead of old’ is detected by
+ ;; the test "losing and collecting concurrently".
+ (wait-operation old)
+ ;; The mutations 'always return the empty list' and 'call loop
+ ;; twice' are detected by test "new lost between making the
+ ;; operation and performing it (2)".
+ ;;
+ ;; The mutation ‘replace old by value’ _survives_ but seems
+ ;; benign.
+ (lambda () (loop old)))
+ ;; 'collect-lost' added something before we started waiting,
+ ;; return it when asked for (unless a race interferes).
+ (make-base-operation
+ #false ; wrap
+ ;; Try (always succeeds).
+ ;; The mutations ‘always return the empty list’ and
+ ;; 'call loop twice' are rejected by test
+ ;; "unreachable + gc -> moved into lost and found".
+ ;;
+ ;; The mutation ‘replace old by value’ _survives_ but seems
+ ;; benign.
+ (lambda () (lambda () (loop old)))
+ ;; There is no block, only try -- try always succeeds.
+ "do not call me, try always returns!")))))
+
+ (define (add-found! lost-and-found lost)
+ "Add an object @var{lost} to @var{lost-and-found}."
+ (%%bind-atomic-boxen
+ ((value (lost-and-found-contents-box lost-and-found) swap!))
+ (let loop ((old value))
+ ;; The mutations ‘simply run the first branch’, ‘simply run
+ ;; the second branch’, ‘run both branches’ and ‘invert the
+ ;; branch condition’ are detected by test "unreachable + gc ->
+ ;; moved into lost and found".
+ ;;
+ ;; TODO: maybe detect replacing ‘old’ by ‘value’.
+ (if (condition? old)
+ ;; Replace the condition by a list containing lost,
+ ;; then notify the condition. This ordering is important,
+ ;; otherwise 'collect-lost-and-found-soperation' could
+ ;; be unnecessarily in the ‘spuriously return the empty list’
+ ;; case, even when there aren't multiple concurrent
+ ;; 'collect-lost-and-found-operation' operations.
+ ;;
+ ;; (Though in practice, this would not seem to be a problem,
+ ;; since 'collect-lost-and-found' is called in a loop anyway.)
+ (let ((new-old (swap! old (list lost))))
+ ;; The mutations ‘invert the branch condition’ and ‘do both
+ ;; branches (in order or out-of-order)’ are detected by the test
+ ;; "unreachable + gc -> moved into lost and found".
+ ;;
+ ;; The mutation ‘simply do the second branch’ is detected by
+ ;; test "new lost between making the operation and performing it
+ ;; (2)" (timeout).
+ ;;
+ ;; The mutation ‘simply do the first branch’ is dected by the
+ ;; test "losing and collecting concurrently" (not 100%
+ ;; reproducible).
+ (if (eq? new-old old)
+ ;; The mutation ‘don't do anything’ is detected by test
+ ;; "new lost between making the operation and performing it
+ ;; (2)" (by timeout).
+ (signal-condition! old)
+ ;; Race was lost, try again!
+ ;;
+ ;; The mutation ‘don't do anything’ is detected by the test
+ ;; "losing and collecting concurrently".
+ ;;
+ ;; The mutation ‘use old instead of new-old’ is detected by
+ ;; the test "losing and collecting concurrently" (infinite
+ ;; loop).
+ ;;
+ ;; The mutation ‘use value instead of new-value’ is
+ ;; _survives_ and seems benign, although possibly suboptimal
+ ;; performance-wise.
+ (loop new-old)))
+ ;; There is already a list of lost objects, extend it.
+ ;; The mutation ‘replace the first old by value’ causes
+ ;; "concurrent losing" to fail. TODO: replacing the second ‘old’
+ ;; is currently undetected.
+ (let ((new-old (swap! old (cons lost old))))
+ ;; The mutations ‘don't do anything’, ‘invert the condition’,
+ ;; ‘replace old by value in the condition’
+ ;; cause the test "concurrent losing" to fail.
+ ;;
+ ;; The mutations ‘always run’ and ‘replace new-old by value in
+ ;; the condition’ cause an infinite loop (presumambly with
+ ;; unbounded memory!). The mutation ‘run loop twice’ seems to
+ ;; cause an OOM or at least very high memory usage.
+ (unless (eq? new-old old)
+ ;; Race was lost, try again!
+ ;;
+ ;; The mutation ‘replace new-old by old’ causes "concurrent
+ ;; losing" to busy hang. The mutation ‘replace new-old by
+ ;; value’ survives and seems benign, although perhaps
+ ;; suboptimal performance-wise.
+ (loop new-old)))))))
+
+ (define *guard* (make-guardian))
+
+ (define-record-type (<losable> make-losable losable?)
+ (fields (immutable lost-and-found losable-lost-and-found))
+ (sealed #false)
+ (protocol (lambda (%make)
+ (lambda (lost-and-found)
+ (assert (lost-and-found? lost-and-found))
+ (let ((object (%make lost-and-found)))
+ (*guard* object)
+ object)))))
+
+ (define (collect-lost)
+ (define object (*guard*))
+ (when object
+ (add-found! (losable-lost-and-found object) object)
+ ;; Absence detected by test
+ ;; "unreachable + gc -> moved into lost and found"
+ (collect-lost)))
+
+ (add-hook! after-gc-hook (lambda () (collect-lost)))))
diff --git a/tests/lost-and-found.scm b/tests/lost-and-found.scm
new file mode 100644
index 0000000..5ff08b2
--- /dev/null
+++ b/tests/lost-and-found.scm
@@ -0,0 +1,312 @@
+;; This file is part of scheme-GNUnet, a partial Scheme port of GNUnet.
+;; Copyright © 2022 GNUnet e.V.
+;;
+;; Scheme-GNUnet is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU Affero General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; Scheme-GNUnet is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; Affero General Public License for more details.
+;;
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+;;
+;; SPDX-License-Identifier: AGPL-3.0-or-later
+
+;; Author: Maxime Devos
+(define-module (test-lost-and-found))
+(import (ice-9 match)
+ (srfi srfi-1)
+ (gnu gnunet concurrency lost-and-found)
+ (rnrs base)
+ (srfi srfi-64)
+ (fibers conditions)
+ (fibers operations)
+ (fibers channels)
+ (rnrs records syntactic)
+ (fibers)
+ (fibers timers)) ; sleep
+
+(test-begin "lost-and-found")
+
+(define-record-type (<losable+datum> make-losable+datum losable+datum?)
+ (parent <losable>)
+ (fields (immutable datum losable-datum))
+ ;; TODO: why is this necessary?
+ (protocol (lambda (%make)
+ (lambda (lost-and-found foo)
+ ((%make lost-and-found) foo)))))
+
+(define (lose lost-and-found start to/exclusive)
+ "Lose integers from the range [start to/exclusive)."
+ (when (< start to/exclusive)
+ (make-losable+datum lost-and-found start)
+ (lose lost-and-found (+ 1 start) to/exclusive)))
+
+(define (collect-operation lost-and-found)
+ "Make an operation returning the list of found integers
+(make sure to gc before performing the operation!)."
+ (wrap-operation
+ (collect-lost-and-found-operation lost-and-found)
+ (lambda (list)
+ (map losable-datum list))))
+
+(define (collect lost-and-found)
+ "Return a list of found integers (make sure to gc first!)."
+ (perform-operation (collect-operation lost-and-found)))
+
+(define (verify collected from to/exclusive)
+ (define count (- to/exclusive from))
+ (define present (make-bitvector count #false))
+ (for-each (lambda (i)
+ (assert (not (bitvector-bit-set? present (- i from))))
+ (bitvector-set-bit! present i))
+ collected)
+ ;; Presumably due to boehmgc being conservative, this number
+ ;; of elements collected tends can be off by one or two.
+ ;; Allow being 5 elements off.
+ (define fraction (/ (bitvector-count present) (- count 5.)))
+ (pk 'f (+ 0.0 fraction))
+ (assert (>= fraction 1))
+ #true
+ #;
+ (receive (collected\expected ∩)
+ (lset-diff+intersection! = collected (iota count from))
+ (assert (null? collected\expected))
+ ;; Presumably due to boehmgc being conservative, this number
+ ;; of elements collected tends can be off by one or two.
+ ;; Allow being 5 elements off.
+ (let ((fraction (/ (length ∩) (- count 5))))
+ (pk 'f (+ 0.0 fraction))
+ (assert (>= fraction 1))
+ #true)))
+
+(define %count 1000)
+(test-assert "unreachable + gc -> moved into lost and found"
+ (let ((lost-and-found (make-lost-and-found)))
+ (lose lost-and-found 0 %count)
+ (gc)
+ (verify (collect lost-and-found) 0 %count)))
+
+(test-assert "new lost between making the operation and performing it (1)"
+ (let ((lost-and-found (make-lost-and-found)))
+ (lose lost-and-found 0 %count)
+ (gc)
+ (define operation (collect-operation lost-and-found))
+ (lose lost-and-found %count (* 2 %count))
+ (gc)
+ (verify (perform-operation operation) 0 (* 2 %count))))
+
+(test-assert "new lost between making the operation and performing it (2)"
+ (let ((lost-and-found (make-lost-and-found)))
+ (lose lost-and-found 0 %count)
+ ;; <- no gc!
+ (define operation (collect-operation lost-and-found))
+ (lose lost-and-found %count (* 2 %count))
+ (gc)
+ (verify (perform-operation operation) 0 (* 2 %count))))
+
+(test-assert "concurrent collecting (light)"
+ (let ((lost-and-found (make-lost-and-found)))
+ (lose lost-and-found 0 %count)
+ (gc)
+ (define operation1 (collect-operation lost-and-found))
+ (define operation2 (collect-operation lost-and-found))
+ (define result1 (perform-operation operation1))
+ ;; Technically, this is allowed to hang (since everything is
+ ;; collected by result1), but due to implementation details,
+ ;; it doesn't.
+ (define result2 (perform-operation operation2))
+ (verify result1 0 %count)
+ (verify (append result1 result2) 0 %count)))
+
+
+;; TODO: copied from (tests update)
+;; TODO: 1e-4 is not sufficient here, 1e-3 is required to make tests
+;; fail (CPU-dependent?).
+(define expected-blocking-operation
+ (wrap-operation (sleep-operation 1e-3) (lambda () 'blocking)))
+
+(test-eq "block while nothing to collect"
+ 'blocking
+ (perform-operation
+ (choice-operation (collect-operation (make-lost-and-found))
+ expected-blocking-operation)))
+
+(test-assert "delaying performing the operation, some concurrency"
+ (let* ((lost-and-found (make-lost-and-found))
+ ;; 'lost-and-found' currently has a condition, so the
+ ;; (if (condition? old) ...) case should happen here
+ (operation (collect-operation lost-and-found)))
+ ;; Trigger and replace the original condition.
+ (lose lost-and-found 0 %count)
+ (gc)
+ (collect lost-and-found)
+ ;; Run the original operation.
+ (define result
+ (perform-operation
+ (choice-operation operation expected-blocking-operation)))
+ ;; The lost objects were already collected, so blocking is fine.
+ ;; There's a form of concurrency, so a spurious empty list is
+ ;; also allowed.
+ (memq result '(blocking ()))))
+
+(define add-found! #{ add-found!}#)
+
+;; There is no rule against the GC hook being called from within the GC hook,
+;; or the GC hook being called in parallel from another thread running the
+;; GC hook, in case a lot of garbage was generated before the original
+;; invocation of the GC hook was able to finish.
+;;
+;; This seems a bit difficult to reliably trigger, so cheat by manually adding
+;; running 'add-found!' concurrently.
+
+(define (lose* lost-and-found start to/exclusive)
+ "Lose integers from the range [start to/exclusive), bypassing the GC and not
+wrap things in a <losable+datum>."
+ (when (< start to/exclusive)
+ (add-found! lost-and-found start)
+ (lose* lost-and-found (+ 1 start) to/exclusive)))
+
+(define (collect* lost-and-found)
+ "Return a list of found integers (no need to GC, since the GC and guardian
was
+bypassed by calling @code{add-found!} directly)."
+ (perform-operation (collect-lost-and-found-operation lost-and-found)))
+
+;; In the current implementation of Guile, while to a degree GC is
parellelised,
+;; gc hooks are serialised (or maybe not, since ‘this hook is run
+(test-assert "concurrent losing"
+ (run-fibers
+ (lambda ()
+ (define %count/fiber 100000)
+ (define fibers 8)
+ (define start (make-condition))
+ (define done-channel (make-channel))
+ (define lost-and-found (make-lost-and-found))
+ (define (lose/async from to/exclusive)
+ (spawn-fiber
+ (lambda ()
+ (wait start)
+ (lose* lost-and-found from to/exclusive)
+ (put-message done-channel 'done))))
+ (let loop ((i 0))
+ (when (< i fibers)
+ (lose/async (* i %count/fiber) (* (+ 1 i) %count/fiber))
+ (loop (+ i 1))))
+ (signal-condition! start)
+ (let loop ((i 0))
+ (when (< i fibers)
+ (get-message done-channel)
+ (loop (+ i 1))))
+ (verify (collect* lost-and-found) 0 (* %count/fiber fibers)))
+ #:install-suspendable-ports? #false ; unnecessary
+ #:hz 10000))
+
+(test-assert "losing and collecting concurrently"
+ (run-fibers
+ (lambda ()
+ ;; 100000 does not suffice for testing the first
+ ;; '(loop new-old)' in 'add-found!'.
+ (define %count/loser 1000000)
+ (define %losers 8)
+ (define %collectors 8)
+ (define start (make-condition))
+ (define done-losing (make-condition))
+ (define done-channel/losers (make-channel))
+ (define done-channel/collectors (make-channel))
+ (define done-losing-operation
+ (wrap-operation
+ (wait-operation done-losing)
+ (lambda () 'done)))
+ (define lost-and-found (make-lost-and-found))
+ (define (lose/async from to/exclusive)
+ (spawn-fiber
+ (lambda ()
+ (wait start)
+ (lose* lost-and-found from to/exclusive)
+ (put-message done-channel/losers 'done))))
+ ;; vector of list of list of collected objects
+ (define collected (make-vector %collectors))
+ (define (collect/async i)
+ (spawn-fiber
+ (lambda ()
+ (wait start)
+ (let loop ((list-of-list-of-results '()))
+ (define r
+ (perform-operation
+ (choice-operation
+ (collect-lost-and-found-operation lost-and-found)
+ done-losing-operation)))
+ (if (eq? r 'done)
+ (begin
+ (vector-set! collected i list-of-list-of-results)
+ (put-message done-channel/collectors 'done))
+ (loop (cons r list-of-list-of-results)))))))
+ ;; Start fibers collecting integers.
+ (let loop ((i 0))
+ (when (< i %collectors)
+ (collect/async i)
+ (loop (+ i 1))))
+ ;; Start fibers losing integers
+ (let loop ((i 0))
+ (when (< i %losers)
+ (lose/async (* i %count/loser) (* (+ 1 i) %count/loser))
+ (loop (+ i 1))))
+ ;; Try starting the collectors and losers start at the same time, to
+ ;; maximise concurrency.
+ (signal-condition! start)
+ (let loop ((i 0))
+ (when (< i %losers)
+ (get-message done-channel/losers)
+ (loop (+ i 1))))
+ (signal-condition! done-losing)
+ ;; Wait for 'collected' to be initialised.
+ (let loop ((i 0))
+ (when (< i %collectors)
+ (get-message done-channel/collectors)
+ (loop (+ i 1))))
+ ;; Do like 'verify' does, without the - 5 because the GC
+ ;; was bypassed.
+ (define results (make-bitvector (* %count/loser %losers)))
+ (define (register-result! i)
+ (assert (not (bitvector-bit-set? results i)))
+ (bitvector-set-bit! results i))
+ (let loop ((i 0))
+ (when (< i %collectors)
+ (for-each
+ (lambda (list)
+ (for-each register-result! list))
+ (vector-ref collected i))
+ (loop (+ i 1))))
+ (define fraction (/ (bitvector-count results) (bitvector-length results)))
+ (pk 'f (+ 0.0 fraction))
+ (assert (>= fraction 1)))
+ #:install-suspendable-ports? #false ; unnecessary
+ #:hz 10000))
+
+(test-assert "lost-and-found as a string (empty)"
+ (let* ((l (make-lost-and-found))
+ (expected (format #f "#<lost-and-found ~x empty>"
+ (object-address l)))
+ (found (object->string l)))
+ (string=? expected found)))
+
+;; It is important to _not_ print the objects inside the lost-and-found,
+;; because <losable> objects keep a lost-and-found in their fields and hence
+;; printing these objects would lead to infinite recursion.
+(test-assert "lost-and-found as a string (non-empty)"
+ (let* ((l (make-lost-and-found))
+ (expected (format #f "#<lost-and-found ~x non-empty>"
+ (object-address l))))
+ (add-found! l (make-losable l))
+ (define found (object->string l))
+ (string=? expected found)))
+
+;; The exception is better raised during the construction of the
+;; <losable> than during the after-gc hook.
+(test-error "make-losable without lost-and-found" (make-losable 'bogus))
+
+(test-end "lost-and-found")
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.