From 5ad317d88d7358ac773e2288b290285a9791e696 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 25 Sep 2021 19:57:11 +0700 Subject: [PATCH 1/4] (#141) EmulsionXmpp: add connection timeout --- Emulsion.Tests/SettingsTests.fs | 1 + Emulsion.Tests/Xmpp/EmulsionXmppTests.fs | 40 +++++++++++++++++++++++ Emulsion/Settings.fs | 3 ++ Emulsion/Xmpp/EmulsionXmpp.fs | 41 ++++++++++++++++++++++-- README.md | 1 + emulsion.example.json | 1 + 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/Emulsion.Tests/SettingsTests.fs b/Emulsion.Tests/SettingsTests.fs index 82a267f5..5e697e40 100644 --- a/Emulsion.Tests/SettingsTests.fs +++ b/Emulsion.Tests/SettingsTests.fs @@ -36,6 +36,7 @@ let private testConfiguration = { Room = "room" RoomPassword = None Nickname = "nickname" + ConnectionTimeout = TimeSpan.FromMinutes 5.0 MessageTimeout = TimeSpan.FromSeconds 30.0 PingInterval = None PingTimeout = TimeSpan.FromSeconds 30.0 diff --git a/Emulsion.Tests/Xmpp/EmulsionXmppTests.fs b/Emulsion.Tests/Xmpp/EmulsionXmppTests.fs index cfbb8175..06318213 100644 --- a/Emulsion.Tests/Xmpp/EmulsionXmppTests.fs +++ b/Emulsion.Tests/Xmpp/EmulsionXmppTests.fs @@ -1,6 +1,8 @@ module Emulsion.Tests.Xmpp.EmulsionXmppTests open System +open System.Diagnostics +open System.Threading open System.Threading.Tasks open JetBrains.Lifetimes @@ -22,6 +24,7 @@ let private settings = { Room = "room@conference.example.org" RoomPassword = None Nickname = "nickname" + ConnectionTimeout = TimeSpan.FromSeconds 30.0 MessageTimeout = TimeSpan.FromSeconds 30.0 PingInterval = None PingTimeout = defaultPingTimeout @@ -70,6 +73,43 @@ type RunTests(outputHelper: ITestOutputHelper) = |> ignore Assert.Equal((settings.Room, settings.Nickname), joinRoomArgs) + [] + member _.``EmulsionXmpp cancels the connection after timeout``(): unit = + let timeout = TimeSpan.FromSeconds 1.0 + let settings = { + settings with + ConnectionTimeout = timeout + } + let client = + XmppClientFactory.create( + connect = fun () -> async { + do! Async.Sleep(timeout * 10.0) + } + ) + let sw = Stopwatch.StartNew() + Assert.Throws(fun () -> runClientSynchronously settings logger client ignore) + |> ignore + Assert.True(sw.Elapsed < timeout * 2.0) + + [] + member _.``EmulsionXmpp forcibly terminates the connection after timeout * 3``(): unit = + let timeout = TimeSpan.FromSeconds 1.0 + let settings = { + settings with + ConnectionTimeout = timeout + } + let client = + XmppClientFactory.create( + connect = fun () -> async { + Thread.Sleep(timeout * 10.0) // non-cancellable + } + ) + let sw = Stopwatch.StartNew() + Assert.Throws(fun () -> runClientSynchronously settings logger client ignore) + |> ignore + Assert.True(sw.Elapsed > timeout) + Assert.True(sw.Elapsed < timeout * 6.0) + type ReceiveMessageTests(outputHelper: ITestOutputHelper) = let logger = Logging.xunitLogger outputHelper diff --git a/Emulsion/Settings.fs b/Emulsion/Settings.fs index 4bd55780..b188e944 100644 --- a/Emulsion/Settings.fs +++ b/Emulsion/Settings.fs @@ -11,6 +11,7 @@ type XmppSettings = { Room: string RoomPassword: string option Nickname: string + ConnectionTimeout: TimeSpan MessageTimeout: TimeSpan PingInterval: TimeSpan option PingTimeout: TimeSpan @@ -31,6 +32,7 @@ type EmulsionSettings = { Log: LogSettings } +let defaultConnectionTimeout = TimeSpan.FromMinutes 5.0 let defaultMessageTimeout = TimeSpan.FromMinutes 5.0 let defaultPingTimeout = TimeSpan.FromSeconds 30.0 @@ -50,6 +52,7 @@ let read (config : IConfiguration) : EmulsionSettings = Room = section.["room"] RoomPassword = Option.ofObj section.["roomPassword"] Nickname = section.["nickname"] + ConnectionTimeout = readTimeSpan defaultConnectionTimeout "connectionTimeout" section MessageTimeout = readTimeSpan defaultMessageTimeout "messageTimeout" section PingInterval = readTimeSpanOpt "pingInterval" section PingTimeout = readTimeSpan defaultPingTimeout "pingTimeout" section diff --git a/Emulsion/Xmpp/EmulsionXmpp.fs b/Emulsion/Xmpp/EmulsionXmpp.fs index e2cee845..00380963 100644 --- a/Emulsion/Xmpp/EmulsionXmpp.fs +++ b/Emulsion/Xmpp/EmulsionXmpp.fs @@ -1,6 +1,8 @@ /// Main business logic for an XMPP part of the Emulsion application. module Emulsion.Xmpp.EmulsionXmpp +open System + open JetBrains.Lifetimes open Serilog open SharpXMPP.XMPP @@ -36,14 +38,49 @@ let initializeLogging (logger: ILogger) (client: IXmppClient): IXmppClient = ) client +let private withTimeout title (logger: ILogger) workflow (timeout: TimeSpan) = async { + logger.Information("Starting \"{Title}\" with timeout {Timeout}.", title, timeout) + let! child = Async.StartChild(workflow, int timeout.TotalMilliseconds) + + let! childWaiter = Async.StartChild(async { + let! _ = child + return Some true + }) + + let waitTime = timeout * 1.5 + let timeoutWaiter = async { + do! Async.Sleep waitTime + return Some false + } + + let! completedInTime = Async.Choice [| childWaiter; timeoutWaiter |] + match completedInTime with + | Some true -> return! child + | _ -> + logger.Information( + "Task {Title} neither complete nor cancelled in {Timeout}. Entering extended wait mode.", + title, + waitTime + ) + let! completedInTime = Async.Choice [| childWaiter; timeoutWaiter |] + match completedInTime with + | Some true -> return! child + | _ -> + logger.Warning( + "Task {Title} neither complete nor cancelled in another {Timeout}. Trying to cancel forcibly by terminating the client.", + title, + waitTime + ) + return raise <| OperationCanceledException($"Operation \"%s{title}\" forcibly cancelled") +} + /// Outer async will establish a connection and enter the room, inner async will await for the room session /// termination. let run (settings: XmppSettings) (logger: ILogger) (client: IXmppClient) (messageReceiver: IncomingMessageReceiver): Async> = async { - logger.Information "Connecting to the server" - let! sessionLifetime = connect client + let! sessionLifetime = withTimeout "server connection" logger (connect client) settings.ConnectionTimeout sessionLifetime.ThrowIfNotAlive() logger.Information "Connection succeeded" diff --git a/README.md b/README.md index c02ff4a8..924e9711 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ settings, there're defaults: { "xmpp": { "roomPassword": null, + "connectionTimeout": "00:05:00", "messageTimeout": "00:05:00", "pingInterval": null, "pingTimeout": "00:00:30" diff --git a/emulsion.example.json b/emulsion.example.json index 20e9d6fe..7cc37d9b 100644 --- a/emulsion.example.json +++ b/emulsion.example.json @@ -5,6 +5,7 @@ "room": "xxxxx@conference.example.org", "roomPassword": "// optional", "nickname": "хортолёт", + "connectionTimeout": "00:05:00", "messageTimeout": "00:05:00", "pingInterval": "00:01:00", "pingTimeout": "00:00:05" From e65ceabe4598c8b4ecf4d1a8e8f6b6452028f1ee Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 25 Sep 2021 22:20:32 +0700 Subject: [PATCH 2/4] (#141) XMPP: update SharpXMPP to 0.3.0 This version has better connection timeout support. --- Emulsion/Emulsion.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emulsion/Emulsion.fsproj b/Emulsion/Emulsion.fsproj index 9be70ded..d8fa0402 100644 --- a/Emulsion/Emulsion.fsproj +++ b/Emulsion/Emulsion.fsproj @@ -41,6 +41,6 @@ - + \ No newline at end of file From 70143900d3e29317ea391c06f9c11250c16fe4f2 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 25 Sep 2021 22:20:47 +0700 Subject: [PATCH 3/4] (#141) Docs: update the changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd15e57c..931b4bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). ### Changed - Additional logging for mailbox processor errors +### Added +- XMPP: connection timeout support ([#141](https://github.com/codingteam/emulsion/issues/141)) + ## [1.7.0] - 2021-06-29 ### Changed - Runtime: upgrade to .NET 5 From b639ca2f3f5087303bebf10e31fb6d2ad1fbe327 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sun, 26 Sep 2021 12:47:45 +0700 Subject: [PATCH 4/4] (#141) EmulsionXmpp: avoid waiting for child workflow when not required --- Emulsion/Xmpp/EmulsionXmpp.fs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emulsion/Xmpp/EmulsionXmpp.fs b/Emulsion/Xmpp/EmulsionXmpp.fs index 00380963..2cefd542 100644 --- a/Emulsion/Xmpp/EmulsionXmpp.fs +++ b/Emulsion/Xmpp/EmulsionXmpp.fs @@ -43,19 +43,19 @@ let private withTimeout title (logger: ILogger) workflow (timeout: TimeSpan) = a let! child = Async.StartChild(workflow, int timeout.TotalMilliseconds) let! childWaiter = Async.StartChild(async { - let! _ = child - return Some true + let! result = child + return Some(ValueSome result) }) let waitTime = timeout * 1.5 let timeoutWaiter = async { do! Async.Sleep waitTime - return Some false + return Some ValueNone } let! completedInTime = Async.Choice [| childWaiter; timeoutWaiter |] match completedInTime with - | Some true -> return! child + | Some(ValueSome r) -> return r | _ -> logger.Information( "Task {Title} neither complete nor cancelled in {Timeout}. Entering extended wait mode.", @@ -64,7 +64,7 @@ let private withTimeout title (logger: ILogger) workflow (timeout: TimeSpan) = a ) let! completedInTime = Async.Choice [| childWaiter; timeoutWaiter |] match completedInTime with - | Some true -> return! child + | Some(ValueSome r) -> return r | _ -> logger.Warning( "Task {Title} neither complete nor cancelled in another {Timeout}. Trying to cancel forcibly by terminating the client.",