diff --git a/.pinned b/.pinned index 8a4794ec89..f7226aed68 100644 --- a/.pinned +++ b/.pinned @@ -1,6 +1,6 @@ bearssl;https://github.com/status-im/nim-bearssl@#667b40440a53a58e9f922e29e20818720c62d9ac chronicles;https://github.com/status-im/nim-chronicles@#32ac8679680ea699f7dbc046e8e0131cac97d41a -chronos;https://github.com/status-im/nim-chronos@#dc3847e4d6733dfc3811454c2a9c384b87343e26 +chronos;https://github.com/status-im/nim-chronos@#ef93a15bf9eb22b3fcc15aab3edaf3f8ae9618a7 dnsclient;https://github.com/ba0f3/dnsclient.nim@#23214235d4784d24aceed99bbfe153379ea557c8 faststreams;https://github.com/status-im/nim-faststreams@#720fc5e5c8e428d9d0af618e1e27c44b42350309 httputils;https://github.com/status-im/nim-http-utils@#3b491a40c60aad9e8d3407443f46f62511e63b18 diff --git a/libp2p/utils/future.nim b/libp2p/utils/future.nim index 61289af0e4..f5dad68ae3 100644 --- a/libp2p/utils/future.nim +++ b/libp2p/utils/future.nim @@ -9,6 +9,7 @@ {.push raises: [].} +import sequtils import chronos type AllFuturesFailedError* = object of CatchableError @@ -31,3 +32,19 @@ proc anyCompleted*[T](futs: seq[Future[T]]): Future[Future[T]] {.async.} = let index = requests.find(raceFut) requests.del(index) + +proc raceAndCancelPending*( + futs: seq[SomeFuture] +): Future[void] {.async: (raises: [ValueError, CancelledError]).} = + ## Executes a race between the provided sequence of futures. + ## Cancels any remaining futures that have not yet completed. + ## + ## - `futs`: A sequence of futures to race. + ## + ## Raises: + ## - `ValueError` if the sequence of futures is empty. + ## - `CancelledError` if the operation is canceled. + try: + discard await race(futs) + finally: + await noCancel allFutures(futs.filterIt(not it.finished).mapIt(it.cancelAndWait)) diff --git a/tests/utils/testfuture.nim b/tests/utils/testfuture.nim new file mode 100644 index 0000000000..c99c77c8ae --- /dev/null +++ b/tests/utils/testfuture.nim @@ -0,0 +1,57 @@ +{.used.} + +# 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 ../helpers +import ../../libp2p/utils/future + +suite "Utils Future": + asyncTest "All Pending Tasks are canceled when returned future is canceled": + proc longRunningTaskA() {.async.} = + await sleepAsync(10.seconds) + + proc longRunningTaskB() {.async.} = + await sleepAsync(10.seconds) + + let + futureA = longRunningTaskA() + futureB = longRunningTaskB() + + # Both futures should be canceled when raceCancel is called + await raceAndCancelPending(@[futureA, futureB]).cancelAndWait() + check futureA.cancelled + check futureB.cancelled + + # Test where one task finishes immediately, leading to the cancellation of the pending task + asyncTest "Cancel Pending Tasks When One Completes": + proc quickTask() {.async.} = + return + + proc slowTask() {.async.} = + await sleepAsync(10.seconds) + + let + futureQuick = quickTask() + futureSlow = slowTask() + + # The quick task finishes, so the slow task should be canceled + await raceAndCancelPending(@[futureQuick, futureSlow]) + check futureQuick.finished + check futureSlow.cancelled + + asyncTest "raceAndCancelPending with AsyncEvent": + let asyncEvent = newAsyncEvent() + let fut1 = asyncEvent.wait() + let fut2 = newAsyncEvent().wait() + asyncEvent.fire() + await raceAndCancelPending(@[fut1, fut2]) + + check fut1.finished + check fut2.cancelled