Skip to content

Commit 58738fc

Browse files
committed
tests
1 parent bd61d96 commit 58738fc

10 files changed

+352
-51
lines changed

.github/workflows/test.yml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Python package
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v2
12+
- uses: actions/setup-go@v2-beta
13+
with:
14+
go-version: '1.14' # The Go version to download (if necessary) and use.
15+
- run: go test
16+
17+

README.md

+12-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
# go-mpx
2-
RedisMPX implementation for the Go programming language.
2+
RedisMPX is a Redis Pub/Sub multiplexer library written in multiple languages and [live coded on Twitch](https://twitch.tv/kristoff_it).
3+
4+
The live coding of this implementation is [archived on YouTube](https://www.youtube.com/watch?v=w6CyJ5dlFd4&list=PLVnMKELF_8IRJ37dW799IxkztIm1Dk3Az).
35

46
## Abstract
5-
RedisMPX is a Redis Pub/Sub multiplexer written in multiple languages and livecoded on Twitch.
7+
When bridging multiple application instances through Redis Pub/Sub it's easy to end up needing
8+
support for multiplexing. RedisMPX streamlines this process in a consistent way across multiple
9+
languages by offering a consistent set of features that cover the most common use cases.
610

7-
- [Twitch channel](https://twitch.tv/kristoff_it)
8-
- [YouTube VOD archive](https://www.youtube.com/user/Kappaloris/videos)
11+
The library works under the assumption that you are going to create separate subscriptions
12+
for each client connected to your service (e.g. WebSockets clients):
913

10-
## Status
11-
Main functionality completed. No unit tests have been written as the real
12-
source of complexity is how goroutines and concurrency mix with network I/O etc.
13-
So instead of wasting effort with mostly useless unit tests, I aim to write a fuzzer for that purpose.
14+
- ChannelSubscription allows you to add and remove individual Redis
15+
PubSub channels similarly to how a multi-room chat application would need to.
16+
- PatternSubscription allows you to subscribe to a single Redis Pub/Sub pattern.
17+
- PromiseSubscription allows you to create a networked promise system.
1418

1519
## Features
1620
- Simple channel subscriptions

channel_subscription.go renamed to channel.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ func (s *ChannelSubscription) Add(channels ...string) {
5151
panic("tried to use a closed subscription")
5252
}
5353
for _, ch := range channels {
54-
_, ok := s.channels[ch]
54+
_, ok := s.Channels[ch]
5555
if !ok {
5656
node := list.NewElement(s)
5757
s.mpx.reqCh <- request{subscriptionAdd, ch, node}
58-
s.channels[ch] = node
58+
s.Channels[ch] = node
5959
}
6060
}
6161
}
@@ -68,10 +68,10 @@ func (s *ChannelSubscription) Remove(channels ...string) {
6868
panic("tried to use a closed subscription")
6969
}
7070
for _, ch := range channels {
71-
node, ok := s.channels[ch]
71+
node, ok := s.Channels[ch]
7272
if ok {
7373
s.mpx.reqCh <- request{subscriptionRemove, ch, node}
74-
delete(s.channels, ch)
74+
delete(s.Channels, ch)
7575
}
7676
}
7777
}
@@ -81,12 +81,12 @@ func (s *ChannelSubscription) Clear() {
8181
if s.closed {
8282
panic("tried to use a closed ChannelSubscription")
8383
}
84-
for ch := range s.channels {
84+
for ch := range s.Channels {
8585
s.Remove(ch)
8686
}
8787

8888
// Reset our internal state
89-
s.channels = make(map[string]*list.Element)
89+
s.Channels = make(map[string]*list.Element)
9090
}
9191

9292
// Calls Clear and frees all references from the Multiplexer.

channel_test.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package mpx
2+
3+
import (
4+
"fmt"
5+
"github.com/gomodule/redigo/redis"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestChannel(t *testing.T) {
11+
connBuilder := func() (redis.Conn, error) {
12+
return redis.Dial("tcp", ":6379")
13+
}
14+
15+
conn, err := connBuilder()
16+
if err != nil {
17+
panic(err)
18+
}
19+
20+
multiplexer := New(connBuilder)
21+
22+
messages := make(chan []byte, 100)
23+
onMessage := func(_ string, msg []byte) {
24+
messages <- msg
25+
}
26+
27+
errors := make(chan error, 100)
28+
onDisconnect := func(err error) {
29+
errors <- err
30+
}
31+
32+
activations := make(chan string, 100)
33+
onActivation := func(ch string) {
34+
activations <- ch
35+
}
36+
37+
sub := multiplexer.NewChannelSubscription(onMessage, onDisconnect, onActivation)
38+
39+
// Activation works
40+
{
41+
// Add a Redis Pub/Sub channel to the Subscription
42+
sub.Add("mychannel")
43+
timer := time.NewTimer(3 * time.Second)
44+
select {
45+
case <-activations:
46+
break
47+
case <-timer.C:
48+
t.Errorf("timed out while waiting for the 1st activation")
49+
}
50+
}
51+
52+
// Can receive messages
53+
{
54+
_, err := conn.Do("PUBLISH", "mychannel", "Hello World 1!")
55+
if err != nil {
56+
panic(err)
57+
}
58+
59+
_, err = conn.Do("PUBLISH", "mychannel", "Hello World 2!")
60+
if err != nil {
61+
panic(err)
62+
}
63+
64+
timer := time.NewTimer(3 * time.Second)
65+
for i := 1; i <= 2; i += 1 {
66+
select {
67+
case m := <-messages:
68+
mm := fmt.Sprintf("Hello World %v!", i)
69+
if string(m) != mm {
70+
t.Errorf("Messages are not equal. Got [%v], expected [%v]", string(m), mm)
71+
}
72+
case <-timer.C:
73+
t.Errorf("timed out while waiting for messages")
74+
}
75+
}
76+
}
77+
78+
}

multiplexer.go

+29-36
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package mpx
77

88
import (
99
"errors"
10-
"fmt"
1110
"github.com/RedisMPX/go-mpx/internal"
1211
"github.com/RedisMPX/go-mpx/internal/list"
1312
"github.com/gomodule/redigo/redis"
@@ -57,15 +56,17 @@ type request struct {
5756
type Multiplexer struct {
5857
// Options
5958
pingTimeout time.Duration
60-
minBackOff time.Duration
61-
maxBackOff time.Duration
59+
minBackoff time.Duration
60+
maxBackoff time.Duration
6261
pipeliningSlots []request
6362

6463
// state
6564
createConn func() (redis.Conn, error)
6665
pubsub redis.PubSubConn
6766
channels map[string]*list.List
6867
patterns map[string]*list.List
68+
active_channels map[string]struct{}
69+
active_patterns map[string]struct{}
6970
reqCh chan request
7071
stop chan struct{}
7172
exit chan struct{}
@@ -100,20 +101,22 @@ func New(createConn func() (redis.Conn, error)) *Multiplexer {
100101
func NewWithOpts(
101102
createConn func() (redis.Conn, error),
102103
pingTimeout time.Duration,
103-
minBackOff time.Duration,
104-
maxBackOff time.Duration,
104+
minBackoff time.Duration,
105+
maxBackoff time.Duration,
105106
messagesBufSize uint,
106107
pipeliningBufSize uint,
107108
) *Multiplexer {
108109
mpx := Multiplexer{
109110
pingTimeout: pingTimeout,
110-
minBackoff: minBackOff,
111+
minBackoff: minBackoff,
111112
maxBackoff: maxBackoff,
112113
pipeliningSlots: make([]request, pipeliningBufSize),
113114
pubsub: redis.PubSubConn{Conn: nil},
114115
createConn: createConn,
115116
channels: make(map[string]*list.List),
116117
patterns: make(map[string]*list.List),
118+
active_channels: make(map[string]struct{}),
119+
active_patterns: make(map[string]struct{}),
117120
reqCh: make(chan request, 100),
118121
stop: make(chan struct{}),
119122
exit: make(chan struct{}),
@@ -267,7 +270,7 @@ func (mpx *Multiplexer) openNewConnection() {
267270
break
268271
}
269272
if errorCount > 0 {
270-
time.Sleep(internal.RetryBackoff(errorCount, mpx.minBackOff, mpx.maxBackOff))
273+
time.Sleep(internal.RetryBackoff(errorCount, mpx.minBackoff, mpx.maxBackoff))
271274
}
272275
errorCount++
273276
}
@@ -301,6 +304,10 @@ func (mpx *Multiplexer) connectionGoroutine() {
301304
return
302305
}
303306
}
307+
308+
// Reset the active channel / pattern sets.
309+
mpx.active_channels = make(map[string]struct{})
310+
mpx.active_patterns = make(map[string]struct{})
304311
}
305312

306313
// Start an "emergency" goroutine that keeps processing
@@ -458,27 +465,21 @@ func (mpx *Multiplexer) processRequests(requests []request, networkIO bool) erro
458465
// Subscribe in Redis
459466
sub = append(sub, req.name)
460467
}
461-
} else {
462-
// We were already subscribed to that channel
463-
if networkIO {
464-
// If we are not in offline-mode, we immediately send confirmation
465-
// that the subscription is active. If we are in the case where
466-
// we just lost connectivity and processRequestGoroutine has not yet
467-
// noticed, we will send a wrong notification, but we will also soon
468-
// send a onDisconnect notification, after this goroutine exits, thus
469-
// rectifying our wrong communication.
470-
if onActivation := req.node.Value.(*ChannelSubscription).onActivation; onActivation != nil {
471-
onActivation(req.name)
472-
}
468+
}
469+
470+
// Trigger onActivation if the subscription is already active.
471+
if _, active := mpx.active_channels[req.name]; active {
472+
if onActivation := req.node.Value.(*ChannelSubscription).onActivation; onActivation != nil {
473+
onActivation(req.name)
473474
}
474475
}
476+
475477
case subscriptionRemove:
476478
if listeners := req.node.DetachFromList(); listeners != nil {
477479
if listeners.Len() > 0 {
478-
fmt.Printf("[ws] unsubbed but more remaining (%v)\n", listeners.Len())
479480
} else {
480-
fmt.Printf("[ws] unsubbed also from Redis\n")
481481
delete(mpx.channels, req.name)
482+
delete(mpx.active_channels, req.name)
482483
if networkIO {
483484
unsub = append(unsub, req.name)
484485
}
@@ -509,18 +510,12 @@ func (mpx *Multiplexer) processRequests(requests []request, networkIO bool) erro
509510
// PSubscribe in Redis
510511
psub = append(psub, req.name)
511512
}
512-
} else {
513-
// We were already subscribed to that channel
514-
if networkIO {
515-
// If we are not in offline-mode, we immediately send confirmation
516-
// that the subscription is active. If we are in the case where
517-
// we just lost connectivity and processRequestGoroutine has not yet
518-
// noticed, we will send a wrong notification, but we will also soon
519-
// send a onDisconnect notification, after this goroutine exits, thus
520-
// rectifying our wrong communication.
521-
if onActivation := req.node.Value.(*PatternSubscription).onActivation; onActivation != nil {
522-
onActivation(req.name)
523-
}
513+
}
514+
515+
// Trigger onActivation if the subscription is already active.
516+
if _, active := mpx.active_patterns[req.name]; active {
517+
if onActivation := req.node.Value.(*PatternSubscription).onActivation; onActivation != nil {
518+
onActivation(req.name)
524519
}
525520
}
526521
case patternClose:
@@ -531,10 +526,9 @@ func (mpx *Multiplexer) processRequests(requests []request, networkIO bool) erro
531526
onMessageNode := req.node.Value.(*PatternSubscription).onMessageNode
532527
if listeners := onMessageNode.DetachFromList(); listeners != nil {
533528
if listeners.Len() > 0 {
534-
fmt.Printf("[ws] unsubbed but more remaining (%v)\n", listeners.Len())
535529
} else {
536-
fmt.Printf("[ws] unsubbed also from Redis\n")
537530
delete(mpx.patterns, req.name)
531+
delete(mpx.active_patterns, req.name)
538532
if networkIO {
539533
punsub = append(punsub, req.name)
540534
}
@@ -711,7 +705,6 @@ func (mpx *Multiplexer) messageReadingGoroutine(size int) {
711705
// TODO: ask redigo do accept pings also from the pubsub
712706
// interface.
713707
if !strings.HasSuffix(msg.Error(), "got type string") {
714-
fmt.Printf("error in Pubsub: %v", msg)
715708
mpx.triggerReconnect(msg)
716709
return
717710
}

multiplexer_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package mpx
2+
3+
import (
4+
"github.com/gomodule/redigo/redis"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestMultiplexer(t *testing.T) {
10+
connBuilder := func() (redis.Conn, error) {
11+
return redis.Dial("tcp", ":6379")
12+
}
13+
14+
conn, err := connBuilder()
15+
if err != nil {
16+
panic(err)
17+
}
18+
19+
multiplexer := New(connBuilder)
20+
21+
messages := make(chan []byte, 100)
22+
onMessage := func(_ string, msg []byte) {
23+
messages <- msg
24+
}
25+
26+
errors := make(chan error, 100)
27+
onDisconnect := func(err error) {
28+
errors <- err
29+
}
30+
31+
activations := make(chan string, 100)
32+
onActivation := func(ch string) {
33+
activations <- ch
34+
}
35+
36+
sub := multiplexer.NewChannelSubscription(onMessage, onDisconnect, onActivation)
37+
38+
// Activation works
39+
{
40+
// Add a Redis Pub/Sub channel to the Subscription
41+
sub.Add("mychannel")
42+
timer := time.NewTimer(3 * time.Second)
43+
select {
44+
case <-activations:
45+
break
46+
case <-timer.C:
47+
t.Errorf("timed out while waiting for the 1st activation")
48+
}
49+
}
50+
51+
// Able to reconnect
52+
{
53+
_, err := conn.Do("client", "kill", "type", "pubsub")
54+
if err != nil {
55+
panic(err)
56+
}
57+
58+
timer := time.NewTimer(3 * time.Second)
59+
60+
select {
61+
case <-errors:
62+
break
63+
case <-timer.C:
64+
t.Errorf("timed out while waiting for the error notification")
65+
}
66+
67+
select {
68+
case <-activations:
69+
break
70+
case <-timer.C:
71+
t.Errorf("timed out while waiting for the 2nd activation")
72+
}
73+
}
74+
75+
}
File renamed without changes.

0 commit comments

Comments
 (0)