diff --git a/libp2p/utils/future.nim b/libp2p/utils/future.nim new file mode 100644 index 000000000..6c18af85c --- /dev/null +++ b/libp2p/utils/future.nim @@ -0,0 +1,35 @@ +# Nim-Libp2p +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +when (NimMajor, NimMinor) < (1, 4): + {.push raises: [Defect].} +else: + {.push raises: [].} + +import chronos + +type + AllFuturesFailedError* = object of CatchableError + +proc anyCompleted*[T](futs: seq[Future[T]]): Future[Future[T]] {.async.} = + ## Returns a future that will complete with the first future that completes. + ## If all futures fail or futs is empty, the returned future will fail with AllFuturesFailedError. + + var requests = futs + + while true: + if requests.len == 0: + raise newException(AllFuturesFailedError, "None of the futures completed successfully") + + var raceFut = await one(requests) + if raceFut.completed: + return raceFut + + let index = requests.find(raceFut) + requests.del(index) diff --git a/tests/testfuture.nim b/tests/testfuture.nim new file mode 100644 index 000000000..8ea378f4b --- /dev/null +++ b/tests/testfuture.nim @@ -0,0 +1,71 @@ +# Nim-Libp2p +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import chronos + +import ../libp2p/utils/future +import ./helpers + +suite "Future": + asyncTest "anyCompleted must complete with first completed future": + proc fut1() {.async.} = + await sleepAsync(100.milliseconds) + + proc fut2() {.async.} = + await sleepAsync(200.milliseconds) + + proc fut3() {.async.} = + raise newException(CatchableError, "fut3") + + var f1 = fut1() + var f2 = fut2() + var f3 = fut3() + var f = await anyCompleted(@[f1, f2, f3]) + + check f == f1 + + asyncTest "anyCompleted must fail with empty list": + expect AllFuturesFailedError: + discard await anyCompleted(newSeq[Future[void]]()) + + asyncTest "anyCompleted must fail if all futures fail": + proc fut1() {.async.} = + raise newException(CatchableError, "fut1") + + proc fut2() {.async.} = + raise newException(CatchableError, "fut2") + + proc fut3() {.async.} = + raise newException(CatchableError, "fut3") + + var f1 = fut1() + var f2 = fut2() + var f3 = fut3() + + expect AllFuturesFailedError: + discard await anyCompleted(@[f1, f2, f3]) + + asyncTest "anyCompleted with timeout": + proc fut1() {.async.} = + await sleepAsync(100.milliseconds) + + proc fut2() {.async.} = + await sleepAsync(200.milliseconds) + + proc fut3() {.async.} = + raise newException(ValueError, "fut3") + + var f1 = fut1() + var f2 = fut2() + var f3 = fut3() + var f = anyCompleted(@[f1, f2, f3]) + if not await f.withTimeout(50.milliseconds): + f.cancel() + + check f.cancelled() diff --git a/tests/testnative.nim b/tests/testnative.nim index 71d122015..b6768e72a 100644 --- a/tests/testnative.nim +++ b/tests/testnative.nim @@ -5,7 +5,8 @@ import testvarint, testminprotobuf, teststreamseq, testsemaphore, - testheartbeat + testheartbeat, + testfuture import testminasn1, testrsa,