LanguageClient icon indicating copy to clipboard operation
LanguageClient copied to clipboard

Question about EventSequence

Open Wouter01 opened this issue 1 year ago • 8 comments

Hi,

I was refactoring some of my code and stumbled upon a weird issue: I couldn't get events from eventSequence (in RestartingServer) anymore. When I added a delay, it works fine. Turns out, it was because I accidentally started the task containing the for await _ in eventSequence twice, and cancelled the first one. Cancelling a task will cancel the streams inside it too, therefore cancelling eventSequence. A cancelled stream cannot be restarted, removing the ability to get any more events from the language server, as I cannot find a way to make a new EventSequence with the public API.

I was wondering if this falls under "intended behavior"? Could it be beneficial to have the ability to create multiple EventSequence instances, so a sequence can be cancelled and multiple tasks can get the events?

Wouter01 avatar Feb 26 '24 14:02 Wouter01

This wasn't the intentional behavior. What do you think should happen here? And, do you know why adding a delay helped?

If you are talking about multiple consumers of eventSequence, that is, sadly, not supported by AsyncSequence today. But, it is desperately needed, in my opinion. https://github.com/apple/swift-async-algorithms/issues/110

mattmassicotte avatar Mar 02 '24 12:03 mattmassicotte

In my case, adding the delay mostly worked because then I would only start listening to the stream once, so I wouldn't have the issue.

You're right that it isn't possible at the moment to broadcast the events. I guess it could be done by using another method, however this isn't a huge issue so I don't think it's needed

Wouter01 avatar Mar 03 '24 12:03 Wouter01

The behavior isn't good, but it's more about how AsyncSequence works than this library. If you do not agree, please re-open!

mattmassicotte avatar Mar 21 '24 19:03 mattmassicotte

The problem is eventSequence is an instance var of a class but it should be a computed var so it can be created by multiple clients, since that is the how they are designed to work. E.g. you can create a streams for NotificationCenter notifications all over your app and filter or process each one how you like, you aren't limited to one notification stream. I noticed the CoreLocation team has made the same design flaw in their CLMonitor events stream.

malhal avatar May 01 '24 17:05 malhal

Ok, I'm listening! If eventSequence creates a new sequence on invocation, it does fix this issue client side. Right now the design uses AsyncStream, so there would have to a collection of continuations somewhere. How should that work?

mattmassicotte avatar May 01 '24 19:05 mattmassicotte

Continuations are for bridging between delegates/closures to async. Since you say your design already is async perhaps you could have a collection of Channels, one per client:

Then you might also like to wrap each Channel in an AsyncSequence similar to how streams are usually wrapped in sequences for public facing APIs: https://forums.swift.org/t/how-do-you-completely-cancel-an-asyncstream/53733/2

I'll give this design a try tomorrow to see if it fixes the issue with CLMonitor and report back.

malhal avatar May 01 '24 22:05 malhal

Here is an implementation that did choose to use a collection of continuations: https://github.com/reddavis/Asynchrone/blob/main/Sources/Asynchrone/Sequences/SharedAsyncSequence.swift

malhal avatar May 01 '24 23:05 malhal

I've looked into this and learned the best way to implement multiple consumers of the eventSequence is for the client to hold an array of AsyncChannels and send the event to all of them. This design worked fine for CLMonitor:

One place:

@MainActor
class MonitorController {
    let monitor: CLMonitor
    var channels: [AsyncChannel<CLMonitor.Event>] = []

    init() async {
        monitor = await CLMonitor("SampleMonitor")
    }
    
    func start() async {
        print("Started")
        do {
            for try await event in await monitor.events {
                for channel in channels {
                    await channel.send(event)
                }
            }
        }catch {}
        print("Ended")
    }

Many places:

        .task {
            print("started")
            let channel = AsyncChannel<CLMonitor.Event>()
            monitorController.channels.append(channel)
            for await event in channel {
                print("event")
            }
            monitorController.channels.removeAll { e in
                e === channel
            }
        }

Since AsyncChannel is a class, it's best for the client to manage that reference. Trying from inside the server is error-prone and might be an anti-pattern anyway. So you can probably just close this issue again 😉

malhal avatar May 03 '24 09:05 malhal

Thanks for all your investigations and input here. I do appreciate it!

mattmassicotte avatar Jul 03 '24 11:07 mattmassicotte