tangled
alpha
login
or
join now
willdot.net
/
message-broker
2
fork
atom
An experimental pub/sub client and server project.
2
fork
atom
overview
issues
pulls
pipelines
Made a subscriber package....kinda words
willdot.net
2 years ago
4eaa21e8
219f554c
+298
-16
8 changed files
expand all
collapse all
unified
split
message.go
server
peer.go
server.go
server_test.go
subscriber.go
topic.go
subscriber
subscriber.go
subscriber_test.go
+6
message.go
···
1
1
+
package messagebroker
2
2
+
3
3
+
type Message struct {
4
4
+
Topic string `json:"topic"`
5
5
+
Data []byte `json:"data"`
6
6
+
}
+1
-1
peer.go
server/peer.go
···
1
1
-
package messagebroker
1
1
+
package server
2
2
3
3
import (
4
4
"encoding/binary"
+5
-8
server.go
server/server.go
···
1
1
-
package messagebroker
1
1
+
package server
2
2
3
3
import (
4
4
"context"
···
8
8
"net"
9
9
"strings"
10
10
"sync"
11
11
+
12
12
+
"github.com/willdot/messagebroker"
11
13
)
12
14
13
15
// Action represents the type of action that a peer requests to do
···
19
21
Publish Action = 3
20
22
)
21
23
22
22
-
type Message struct {
23
23
-
Topic string `json:"topic"`
24
24
-
Data []byte `json:"data"`
25
25
-
}
26
26
-
27
24
type Server struct {
28
25
addr string
29
26
lis net.Listener
···
32
29
topics map[string]topic
33
30
}
34
31
35
35
-
func NewServer(ctx context.Context, addr string) (*Server, error) {
32
32
+
func New(ctx context.Context, addr string) (*Server, error) {
36
33
lis, err := net.Listen("tcp", addr)
37
34
if err != nil {
38
35
return nil, fmt.Errorf("failed to listen: %w", err)
···
204
201
return
205
202
}
206
203
207
207
-
var msg Message
204
204
+
var msg messagebroker.Message
208
205
err = json.Unmarshal(buf, &msg)
209
206
if err != nil {
210
207
_, _ = peer.Write([]byte("invalid message"))
+5
-4
server_test.go
server/server_test.go
···
1
1
-
package messagebroker
1
1
+
package server
2
2
3
3
import (
4
4
"context"
···
11
11
12
12
"github.com/stretchr/testify/assert"
13
13
"github.com/stretchr/testify/require"
14
14
+
"github.com/willdot/messagebroker"
14
15
)
15
16
16
17
func createServer(t *testing.T) *Server {
17
17
-
srv, err := NewServer(context.Background(), ":3000")
18
18
+
srv, err := New(context.Background(), ":3000")
18
19
require.NoError(t, err)
19
20
20
21
t.Cleanup(func() {
···
205
206
require.NoError(t, err)
206
207
207
208
// send a message
208
208
-
msg := Message{
209
209
+
msg := messagebroker.Message{
209
210
Topic: "topic a",
210
211
Data: []byte("hello world"),
211
212
}
···
247
248
248
249
messages := make([][]byte, 0, 10)
249
250
for i := 0; i < 10; i++ {
250
250
-
msg := Message{
251
251
+
msg := messagebroker.Message{
251
252
Topic: "topic a",
252
253
Data: []byte(fmt.Sprintf("message %d", i)),
253
254
}
+1
-1
subscriber.go
server/subscriber.go
···
1
1
-
package messagebroker
1
1
+
package server
2
2
3
3
import (
4
4
"encoding/binary"
+137
subscriber/subscriber.go
···
1
1
+
package subscriber
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"encoding/binary"
6
6
+
"encoding/json"
7
7
+
"fmt"
8
8
+
"log/slog"
9
9
+
"net"
10
10
+
"time"
11
11
+
12
12
+
"github.com/willdot/messagebroker"
13
13
+
"github.com/willdot/messagebroker/server"
14
14
+
)
15
15
+
16
16
+
type Subscriber struct {
17
17
+
conn net.Conn
18
18
+
}
19
19
+
20
20
+
func New(addr string) (*Subscriber, error) {
21
21
+
conn, err := net.Dial("tcp", addr)
22
22
+
if err != nil {
23
23
+
return nil, fmt.Errorf("failed to dial: %w", err)
24
24
+
}
25
25
+
26
26
+
return &Subscriber{
27
27
+
conn: conn,
28
28
+
}, nil
29
29
+
}
30
30
+
31
31
+
func (s *Subscriber) Close() error {
32
32
+
return s.conn.Close()
33
33
+
}
34
34
+
35
35
+
func (s *Subscriber) SubscribeToTopics(topicNames []string) error {
36
36
+
err := binary.Write(s.conn, binary.BigEndian, server.Subscribe)
37
37
+
if err != nil {
38
38
+
return fmt.Errorf("failed to subscribe: %w", err)
39
39
+
}
40
40
+
41
41
+
b, err := json.Marshal(topicNames)
42
42
+
if err != nil {
43
43
+
return fmt.Errorf("failed to marshal topic names: %w", err)
44
44
+
}
45
45
+
46
46
+
err = binary.Write(s.conn, binary.BigEndian, uint32(len(b)))
47
47
+
if err != nil {
48
48
+
return fmt.Errorf("failed to write topic data length: %w", err)
49
49
+
}
50
50
+
51
51
+
_, err = s.conn.Write(b)
52
52
+
if err != nil {
53
53
+
return fmt.Errorf("failed to subscribe to topics: %w", err)
54
54
+
}
55
55
+
buf := make([]byte, 512)
56
56
+
_, err = s.conn.Read(buf)
57
57
+
if err != nil {
58
58
+
return fmt.Errorf("failed to read confirmation of subscription: %w", err)
59
59
+
}
60
60
+
61
61
+
// TODO: this is soooo hacky - need to have some sort of response code
62
62
+
if string(buf[:10]) != "subscribed" {
63
63
+
return fmt.Errorf("failed to subscribe: '%s'", string(buf))
64
64
+
}
65
65
+
66
66
+
return nil
67
67
+
}
68
68
+
69
69
+
type Consumer struct {
70
70
+
Msgs chan messagebroker.Message
71
71
+
Err error
72
72
+
}
73
73
+
74
74
+
// TODO: maybe buffer the message channel up?
75
75
+
func (s *Subscriber) Consume(ctx context.Context) *Consumer {
76
76
+
consumer := &Consumer{
77
77
+
Msgs: make(chan messagebroker.Message),
78
78
+
}
79
79
+
80
80
+
go s.consume(ctx, consumer)
81
81
+
82
82
+
return consumer
83
83
+
}
84
84
+
85
85
+
func (s *Subscriber) consume(ctx context.Context, consumer *Consumer) {
86
86
+
defer close(consumer.Msgs)
87
87
+
for {
88
88
+
if ctx.Err() != nil {
89
89
+
return
90
90
+
}
91
91
+
92
92
+
msg, err := s.readMessage()
93
93
+
if err != nil {
94
94
+
consumer.Err = err
95
95
+
return
96
96
+
}
97
97
+
98
98
+
if msg != nil {
99
99
+
consumer.Msgs <- *msg
100
100
+
}
101
101
+
}
102
102
+
}
103
103
+
104
104
+
func (s *Subscriber) readMessage() (*messagebroker.Message, error) {
105
105
+
err := s.conn.SetReadDeadline(time.Now().Add(time.Second))
106
106
+
if err != nil {
107
107
+
return nil, err
108
108
+
}
109
109
+
110
110
+
var dataLen uint64
111
111
+
err = binary.Read(s.conn, binary.BigEndian, &dataLen)
112
112
+
if err != nil {
113
113
+
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
114
114
+
return nil, nil
115
115
+
}
116
116
+
return nil, err
117
117
+
}
118
118
+
119
119
+
if dataLen <= 0 {
120
120
+
return nil, nil
121
121
+
}
122
122
+
123
123
+
buf := make([]byte, dataLen)
124
124
+
_, err = s.conn.Read(buf)
125
125
+
if err != nil {
126
126
+
return nil, err
127
127
+
}
128
128
+
129
129
+
var msg messagebroker.Message
130
130
+
err = json.Unmarshal(buf, &msg)
131
131
+
if err != nil {
132
132
+
slog.Error("failed to unmarshal message", "error", err)
133
133
+
return nil, nil
134
134
+
}
135
135
+
136
136
+
return &msg, nil
137
137
+
}
+139
subscriber/subscriber_test.go
···
1
1
+
package subscriber_test
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"encoding/binary"
6
6
+
"encoding/json"
7
7
+
"fmt"
8
8
+
"net"
9
9
+
"testing"
10
10
+
"time"
11
11
+
12
12
+
"github.com/stretchr/testify/assert"
13
13
+
"github.com/stretchr/testify/require"
14
14
+
"github.com/willdot/messagebroker"
15
15
+
"github.com/willdot/messagebroker/server"
16
16
+
"github.com/willdot/messagebroker/subscriber"
17
17
+
)
18
18
+
19
19
+
const (
20
20
+
serverAddr = ":3000"
21
21
+
)
22
22
+
23
23
+
func createServer(t *testing.T) {
24
24
+
server, err := server.New(context.Background(), serverAddr)
25
25
+
require.NoError(t, err)
26
26
+
27
27
+
t.Cleanup(func() {
28
28
+
server.Shutdown()
29
29
+
})
30
30
+
}
31
31
+
32
32
+
func TestNew(t *testing.T) {
33
33
+
createServer(t)
34
34
+
35
35
+
sub, err := subscriber.New(serverAddr)
36
36
+
require.NoError(t, err)
37
37
+
38
38
+
t.Cleanup(func() {
39
39
+
sub.Close()
40
40
+
})
41
41
+
}
42
42
+
43
43
+
func TestNewInvalidServerAddr(t *testing.T) {
44
44
+
createServer(t)
45
45
+
46
46
+
_, err := subscriber.New(":123456")
47
47
+
require.Error(t, err)
48
48
+
}
49
49
+
50
50
+
func TestSubscribeToTopics(t *testing.T) {
51
51
+
createServer(t)
52
52
+
53
53
+
sub, err := subscriber.New(serverAddr)
54
54
+
require.NoError(t, err)
55
55
+
56
56
+
t.Cleanup(func() {
57
57
+
sub.Close()
58
58
+
})
59
59
+
60
60
+
topics := []string{"topic a", "topic b"}
61
61
+
62
62
+
err = sub.SubscribeToTopics(topics)
63
63
+
require.NoError(t, err)
64
64
+
}
65
65
+
66
66
+
func TestSubscribeConsumeFromSubscription(t *testing.T) {
67
67
+
createServer(t)
68
68
+
69
69
+
sub, err := subscriber.New(serverAddr)
70
70
+
require.NoError(t, err)
71
71
+
72
72
+
t.Cleanup(func() {
73
73
+
sub.Close()
74
74
+
})
75
75
+
76
76
+
topics := []string{"topic a", "topic b"}
77
77
+
78
78
+
err = sub.SubscribeToTopics(topics)
79
79
+
require.NoError(t, err)
80
80
+
81
81
+
ctx, cancel := context.WithCancel(context.Background())
82
82
+
t.Cleanup(func() {
83
83
+
cancel()
84
84
+
})
85
85
+
86
86
+
consumer := sub.Consume(ctx)
87
87
+
require.NoError(t, err)
88
88
+
89
89
+
var receivedMessages []messagebroker.Message
90
90
+
91
91
+
consumerFinCh := make(chan struct{})
92
92
+
go func() {
93
93
+
for msg := range consumer.Msgs {
94
94
+
receivedMessages = append(receivedMessages, msg)
95
95
+
}
96
96
+
97
97
+
require.NoError(t, err)
98
98
+
consumerFinCh <- struct{}{}
99
99
+
}()
100
100
+
101
101
+
publisherConn, err := net.Dial("tcp", "localhost:3000")
102
102
+
require.NoError(t, err)
103
103
+
104
104
+
err = binary.Write(publisherConn, binary.BigEndian, server.Publish)
105
105
+
require.NoError(t, err)
106
106
+
107
107
+
// send some messages
108
108
+
sentMessages := make([]messagebroker.Message, 0, 10)
109
109
+
for i := 0; i < 10; i++ {
110
110
+
msg := messagebroker.Message{
111
111
+
Topic: "topic a",
112
112
+
Data: []byte(fmt.Sprintf("message %d", i)),
113
113
+
}
114
114
+
115
115
+
sentMessages = append(sentMessages, msg)
116
116
+
117
117
+
b, err := json.Marshal(msg)
118
118
+
require.NoError(t, err)
119
119
+
120
120
+
err = binary.Write(publisherConn, binary.BigEndian, uint32(len(b)))
121
121
+
require.NoError(t, err)
122
122
+
n, err := publisherConn.Write(b)
123
123
+
require.NoError(t, err)
124
124
+
require.Equal(t, len(b), n)
125
125
+
}
126
126
+
127
127
+
// give the consumer some time to read the messages -- TODO: make better!
128
128
+
time.Sleep(time.Millisecond * 500)
129
129
+
cancel()
130
130
+
131
131
+
select {
132
132
+
case <-consumerFinCh:
133
133
+
break
134
134
+
case <-time.After(time.Second):
135
135
+
t.Fatal("timed out waiting for consumer to read messages")
136
136
+
}
137
137
+
138
138
+
assert.ElementsMatch(t, receivedMessages, sentMessages)
139
139
+
}
+4
-2
topic.go
server/topic.go
···
1
1
-
package messagebroker
1
1
+
package server
2
2
3
3
import (
4
4
"encoding/json"
5
5
"log/slog"
6
6
"net"
7
7
"sync"
8
8
+
9
9
+
"github.com/willdot/messagebroker"
8
10
)
9
11
10
12
type topic struct {
···
28
30
delete(t.subscriptions, addr)
29
31
}
30
32
31
31
-
func (t *topic) sendMessageToSubscribers(msg Message) {
33
33
+
func (t *topic) sendMessageToSubscribers(msg messagebroker.Message) {
32
34
t.mu.Lock()
33
35
subscribers := t.subscriptions
34
36
t.mu.Unlock()