Skip to content

Commit b1fe42c

Browse files
authored
[Subscription] check already closed channels (#384)
This PR fixes an issue I encountered using subscriptions with genqlient. I've noticed that the Unsubscribe function closes the subscription channel, but never checks if that channel has already been closed. So a snippet like that causes a panic when Close is called: ``` go err = gqlsubcl.Unsubscribe(subscriptionID) if err != nil { return fmt.Errorf("failed to unsubscribe from GraphQL subscription: %w", err) } if err := gqlsubcl.Close(); err != nil { return fmt.Errorf("failed to close GraphQL subscription client: %w", err) } ``` I have: - [x] Written a clear PR title and description (above) - [x] Signed the [Khan Academy CLA](https://www.khanacademy.org/r/cla) - [x] Added tests covering my changes, if applicable - [ ] Included a link to the issue fixed, if applicable - [x] Included documentation, for new features - [x] Added an entry to the changelog
1 parent d973af1 commit b1fe42c

File tree

3 files changed

+71
-1
lines changed

3 files changed

+71
-1
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ This release fixes a bug introduced in v0.8.0 breaking path resolution on Window
4141
- fixed documentation link in `introduction.md`
4242
- upgraded version of alexflint/go-arg from 1.4.2 to 1.5.1
4343
- fixed a typo in the struct + fragment error message
44+
- avoid closing subscription channels more than once, which could cause a panic in some cases
4445

4546
## v0.8.0
4647

graphql/subscription.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ func (s *subscriptionMap) Unsubscribe(subscriptionID string) error {
4444
if !success {
4545
return fmt.Errorf("tried to unsubscribe from unknown subscription with ID '%s'", subscriptionID)
4646
}
47+
hasBeenUnsubscribed := unsub.hasBeenUnsubscribed
4748
unsub.hasBeenUnsubscribed = true
4849
s.map_[subscriptionID] = unsub
49-
reflect.ValueOf(s.map_[subscriptionID].interfaceChan).Close()
50+
51+
if !hasBeenUnsubscribed {
52+
reflect.ValueOf(s.map_[subscriptionID].interfaceChan).Close()
53+
}
5054
return nil
5155
}
5256

graphql/subscription_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package graphql
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func Test_subscriptionMap_Unsubscribe(t *testing.T) {
8+
type args struct {
9+
subscriptionID string
10+
}
11+
tests := []struct {
12+
name string
13+
args args
14+
sm subscriptionMap
15+
wantErr bool
16+
}{
17+
{
18+
name: "unsubscribe existing subscription",
19+
sm: subscriptionMap{
20+
map_: map[string]subscription{
21+
"sub1": {
22+
id: "sub1",
23+
interfaceChan: make(chan struct{}),
24+
forwardDataFunc: nil,
25+
hasBeenUnsubscribed: false,
26+
},
27+
},
28+
},
29+
args: args{subscriptionID: "sub1"},
30+
wantErr: false,
31+
},
32+
{
33+
name: "unsubscribe non-existent subscription",
34+
sm: subscriptionMap{
35+
map_: map[string]subscription{},
36+
},
37+
args: args{subscriptionID: "doesnotexist"},
38+
wantErr: true,
39+
},
40+
{
41+
name: "unsubscribe already unsubscribed subscription",
42+
sm: subscriptionMap{
43+
map_: map[string]subscription{
44+
"sub2": {
45+
id: "sub2",
46+
interfaceChan: nil,
47+
forwardDataFunc: nil,
48+
hasBeenUnsubscribed: true,
49+
},
50+
},
51+
},
52+
args: args{subscriptionID: "sub2"},
53+
wantErr: false,
54+
},
55+
}
56+
for i := range tests {
57+
tt := &tests[i]
58+
t.Run(tt.name, func(t *testing.T) {
59+
s := &tt.sm
60+
if err := s.Unsubscribe(tt.args.subscriptionID); (err != nil) != tt.wantErr {
61+
t.Errorf("subscriptionMap.Unsubscribe() error = %v, wantErr %v", err, tt.wantErr)
62+
}
63+
})
64+
}
65+
}

0 commit comments

Comments
 (0)