suave icon indicating copy to clipboard operation
suave copied to clipboard

EventSource resource exhaustion

Open haf opened this issue 8 years ago • 8 comments

If the client doesn't properly close the EventSource channel, this is what happens after a while:

[W] 2015-11-06T18:13:40.9217220Z: tcp request processing failed [Suave.Tcp.tcpIpServer.job] exn:
System.InvalidOperationException: Stack empty.
   at System.ThrowHelper.ThrowInvalidOperationException (ExceptionResource resource) in <filename unknown>:line 0
   at System.Collections.Generic.Stack`1[T].Pop () in <filename unknown>:line 0
   at Suave.Sockets.BufferManager.PopBuffer (Microsoft.FSharp.Core.FSharpOption`1 context) in <filename unknown>:line 0
   at [email protected] (Microsoft.FSharp.Core.Unit unitVar) in <filename unknown>:line 0
   at Microsoft.FSharp.Control.AsyncBuilderImpl+callA@805[b,a].Invoke (Microsoft.FSharp.Control.AsyncParams`1 args) in <filename unknown>:line 0
[W] 2015-11-06T18:13:41.0627360Z: tcp request processing failed [Suave.Tcp.tcpIpServer.job] exn:
System.InvalidOperationException: Stack empty.
   at System.ThrowHelper.ThrowInvalidOperationException (ExceptionResource resource) in <filename unknown>:line 0
   at System.Collections.Generic.Stack`1[T].Pop () in <filename unknown>:line 0
   at Suave.Sockets.BufferManager.PopBuffer (Microsoft.FSharp.Core.FSharpOption`1 context) in <filename unknown>:line 0
   at [email protected] (Microsoft.FSharp.Core.Unit unitVar) in <filename unknown>:line 0
   at Microsoft.FSharp.Control.AsyncBuilderImpl+callA@805[b,a].Invoke (Microsoft.FSharp.Control.AsyncParams`1 args) in <filename unknown>:line 0

This is because there's no NACK facility in async; should either mimik it with async.Choose with a timeout, so that the socket monad gets 'polled' by having its bind invoked and thereby checking whether the socket is closed.

haf avatar Nov 06 '15 18:11 haf

Hi, is there any known workaround for this? I got web running on Suave (in Docker container in AWS) and after about 2 days I got exactly this error. Redeploy of container seems to be the only way, but that is definitely something you do not want to do on daily basis.

Dzoukr avatar Jan 21 '16 17:01 Dzoukr

You have time out the connection. Search gitter.im for Async.Choose for an example. Sorry about the issue; I have it on my TODO to fix.

haf avatar Jan 21 '16 20:01 haf

That is ok, web is still in preparing phase (and will be for next few days), but having few more projects in mind, this should not happen. Looking forward to see you fixed it on future release.

Dzoukr avatar Jan 22 '16 02:01 Dzoukr

Has this been fixed or should we still expect to restart Suave on daily basis?

slav avatar Nov 17 '16 03:11 slav

It's technically not a bug since the TCP connection is open. We're cycling our EventSource sockets every thirty seconds over here... How do you envision fixing this @slav?

haf avatar Nov 17 '16 10:11 haf

I thought about this once, even coded a connection manager to disconnect connections with no activity. I think it makes sense to have such a connection manager and track time spent per request and similar stats.

ademar avatar Nov 17 '16 18:11 ademar

This is what we have right now; I'd love some feedback and input and perhaps we can create a good component to place in the appropriate module? Obviously this is different because we use Hopac everywhere internally...


    let private checkAndDispose (d : #IDisposable) result =
      async {
        let! r = result
        match r with
        | Choice1Of2 a ->
          return Choice1Of2 a
        | Choice2Of2 e ->
          d.Dispose()
          return Choice2Of2 e
      }

    let private resettingTimeOut timeSpan (d : #IDisposable) =
      let resetCh = Ch()
      let rec inner resetCh =
        Alt.choose [
          Ch.take resetCh |> Alt.afterJob (fun () -> inner resetCh)
          timeOut timeSpan |> Alt.afterFun (fun _ -> d.Dispose())
        ]
      start (inner resetCh)
      resetCh

    // TODO: this currently disposes the stream after 30 seconds staying alive forever
    // This could be replaced by a heartbeat mechanism of sending a raw ':\n' over the
    // stream which the eventstream specification says the client should always ignore
    let inline notificationStreamToWebPart subscribeFun : Connection -> SocketOp<_> =
      fun conn ->
        socket {
          let! (notificationStream : NotificationStream<_>) =
            subscribeFun
            |> Hopac.Job.map Choice1Of2
            |> Job.toAsync
          let reset =
            resettingTimeOut (TimeSpan.FromSeconds 30.) notificationStream
          let guard a = checkAndDispose notificationStream a
          return!
            Hopac.Stream.foldJob
              (fun xxxx x ->
                x |> Json.serialize |> Json.format
                |> EventSource.Message.create (Guid.NewGuid().ToString())
                |> EventSource.send conn
                |> SocketOp.map (fun _ -> conn)
                |> guard
                |> Job.fromAsync
                |> Job.bind (fun r -> job { do! Ch.send reset ()
                                            return r }))
              (Choice1Of2 conn)
              notificationStream.stream
            |> Job.toAsync
        }

haf avatar Nov 18 '16 10:11 haf

@slav Have you had time to consider my question?

haf avatar Nov 26 '16 12:11 haf