replace close() with actually sending a value to .EndOfStoredEvents and .Closed channels.

I thought `close()` would be nice because it would be cheap and not lock the goroutine while waiting for the receiver to acknowledge the thing, but turns out it introduces the serious risk of users putting <- sub.EndOfStoredEvents in the same for { select {} } statement as sub.Events, for example, and they they get into an infinite loop.

we had this same problem here inside this same library, and what is fixed in 242af0bf76 by @mattn.
This commit is contained in:
fiatjaf
2024-01-01 10:16:07 -03:00
parent 3afa6fc5f6
commit 5938a71146
2 changed files with 56 additions and 3 deletions

52
eose_test.go Normal file
View File

@@ -0,0 +1,52 @@
package nostr
import (
"context"
"testing"
"time"
)
func TestEOSEMadness(t *testing.T) {
rl := mustRelayConnect(RELAY)
defer rl.Close()
sub, err := rl.Subscribe(context.Background(), Filters{
{Kinds: []int{KindTextNote}, Limit: 2},
})
if err != nil {
t.Errorf("subscription failed: %v", err)
return
}
timeout := time.After(3 * time.Second)
n := 0
e := 0
for {
select {
case event := <-sub.Events:
if event == nil {
t.Fatalf("event is nil: %v", event)
}
n++
case <-sub.EndOfStoredEvents:
e++
if e > 1 {
t.Fatalf("eose infinite loop")
}
continue
case <-rl.Context().Done():
t.Fatalf("connection closed: %v", rl.Context().Err())
case <-timeout:
goto end
}
}
end:
if e != 1 {
t.Fatalf("didn't get an eose")
}
if n < 2 {
t.Fatalf("didn't get events")
}
}

View File

@@ -106,15 +106,16 @@ func (sub *Subscription) dispatchEose() {
if sub.eosed.CompareAndSwap(false, true) {
go func() {
sub.storedwg.Wait()
close(sub.EndOfStoredEvents)
sub.EndOfStoredEvents <- struct{}{}
}()
}
}
func (sub *Subscription) dispatchClosed(reason string) {
if sub.closed.CompareAndSwap(false, true) {
go func() {
sub.ClosedReason <- reason
close(sub.ClosedReason)
}()
}
}