GenieFramework / Genie.jl

🧞The highly productive Julia web framework
https://genieframework.com
MIT License
2.26k stars 190 forks source link

ArgumentError("send() requires `!(ws.writeclosed)`") #659

Open wiseac opened 1 year ago

wiseac commented 1 year ago

In these dev logs, i keep getting this error.

Error: 2023-07-26 16:14:02 ArgumentError("send() requires!(ws.writeclosed)") └ @ Genie.WebChannels /home/crd/.julia/packages/Genie/XpFvB/src/WebChannels.jl:239

It happens pretty often and I'm not sure why? ┌ Error: 2023-07-26 16:14:02 ArgumentError("send() requires!(ws.writeclosed)") └ @ Genie.WebChannels /home/crd/.julia/packages/Genie/XpFvB/src/WebChannels.jl:239 ┌ Error: 2023-07-26 16:14:53 ArgumentError("send() requires!(ws.writeclosed)") └ @ Genie.WebChannels /home/crd/.julia/packages/Genie/XpFvB/src/WebChannels.jl:239 ┌ Error: 2023-07-26 16:15:02 ArgumentError("send() requires!(ws.writeclosed)") └ @ Genie.WebChannels /home/crd/.julia/packages/Genie/XpFvB/src/WebChannels.jl:239

I am running a global model webpage and have a for loop running for long periods at a time 1 hr+.

Best, W

essenciary commented 1 year ago

@wiseac I've never seen this one. Can you provide a small example of how to replicate this? Thanks

essenciary commented 1 year ago

Closing due to feedback. Please reopen if you can provide more info.

zygmuntszpak commented 8 months ago

I'm also encountering the same error but in a rather unpredictable fashion (hence difficult to reproduce). Comparing the Genie log with the console message in the browser I noticed the following:

send requires not write closed

and then in the Console at around the same timestamp

invalid frame header

However, the same error appears later in the Console

invalid frame header again

but this time there is no concomitant error in Genie.

Moreover, there are other errors in the Console such as

could not decode text

which sometimes do not give rise to any error in Genie, but at other times do as in this example:

could not decode text again send requires not write closed again

I don't know if concurrency is related to the issue, but in my use case I am continually reading a stream of MQTT messages and updating reactive plots every second. The code to retrieve the latest MQTT messages from a database and update the reactive variables is running in its own task.

Regarding the "Could not decode a text frame as UTF-8" there is a discussion of some possible solutions here https://stackoverflow.com/questions/17126227/could-not-decode-a-text-frame-as-utf-8 but I'm not sure if this is a Websockets.jl issue or a Genie issue.

essenciary commented 8 months ago

Looks like the socket connection is closed when Genie attempts to push some data. Hence the error message which could be read as sending requires that the websocket is not closed (while in fact it is closed).

Btw, that error is not thrown by Genie, but comes from WebSockets/HTTP package.

hhaensel commented 7 months ago

I encountered a very similar issue when sending messages to the websocket asynchronously.

In normal circumstances this doesn't happen. But if you have external async events triggering updates then this can occur. The root cause is that Socket.send(ws, message) doesn't protect the ws from being written to by other processes. When this happens, the two messages get mixed and the messages are misunderstood.

@essenciary

We could solve this by a setup like the following:

using Genie.WebChannels.HTTP.WebSockets
using Sockets

const WS_QUEUES = Dict{UInt, Channel{Tuple{String, Channel{Nothing}}}}()

function message_safe(clientid, message::String)
    ws = Genie.WebChannels.CLIENTS[clientid].client
    future = Channel{Nothing}(1)

    q = get!(WS_QUEUES, clientid) do
        println("Setting up websocket queue for $(repr(clientid)) ...")
        queue = Channel{Tuple{String, Channel{Nothing}}}(10)
        t = @async while true
            mymessage, myfuture = take!(queue)
            try
                Sockets.send(ws, mymessage)
            finally
                println("future: $myfuture")
                println(mymessage)
                put!(myfuture, nothing)
            end
        end
        queue
    end

    put!(q, (message, future))

    take!(future) # Wait until the message is processed
end

If you want to test the approach, simply start any Genie App, open the dev tools to observe the browser's console and execute the following

say_secure(clientid, x) = message_safe(clientid,
    Genie.WebChannels.tagbase64encode(""">eval:console.log($(js_attr(x)))""")
)

say(ws::WebSocket, x) = send(ws,
    Genie.WebChannels.tagbase64encode(""">eval:console.log($(js_attr(x)))""")
)

say(clientid::UInt, x::String) = say(Genie.WebChannels.CLIENTS[clientid].client, x)

ws = Genie.WebChannels.clients()[1].client
client = Genie.WebChannels.id(ws)

say(client, "hello")
# will say 'hello' at the browser console

say_secure(client, "hello2")
# will say 'hello2' at the browser console

# this will work
for i in 1:20
    @async say_secure(client, "hello $i you")
end

# this will produce the UTF-8 error as described in the issue above
for i in 1:20
    @async say(client, "hello $i")
end

Here I set up the handler during the first send() but we could make it part of the subscription.

essenciary commented 7 months ago

@hhaensel thanks! It would make sense to have this as the default - would you like to make a PR please?