An experimental pub/sub client and server project.

Refactor. Huge refactor to make conns synchronous

+2 -1
.gitignore
··· 1 - .DS_STORE 1 + .DS_STORE 2 + example/example
+20
dockerfile.example-server
··· 1 + FROM golang:latest as builder 2 + 3 + WORKDIR /app 4 + 5 + COPY go.mod go.sum ./ 6 + COPY example/server/ ./ 7 + RUN go mod download 8 + 9 + COPY . . 10 + 11 + RUN CGO_ENABLED=0 go build -o message-broker-server . 12 + 13 + FROM alpine:latest 14 + 15 + RUN apk --no-cache add ca-certificates 16 + 17 + WORKDIR /root/ 18 + COPY --from=builder /app/message-broker-server . 19 + 20 + CMD ["./message-broker-server"]
+9 -9
example/main.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "flag" 5 6 "fmt" 6 7 "log/slog" 7 8 8 - "github.com/willdot/messagebroker" 9 9 "github.com/willdot/messagebroker/pubsub" 10 - "github.com/willdot/messagebroker/server" 11 10 ) 12 11 12 + var consumeOnly *bool 13 + 13 14 func main() { 14 - server, err := server.New(context.Background(), ":3000") 15 - if err != nil { 16 - panic(err) 17 - } 18 - defer server.Shutdown() 15 + consumeOnly = flag.Bool("consume-only", false, "just consumes (doesn't start server and doesn't publish)") 16 + flag.Parse() 19 17 20 - go sendMessages() 18 + if *consumeOnly == false { 19 + go sendMessages() 20 + } 21 21 22 22 sub, err := pubsub.NewSubscriber(":3000") 23 23 if err != nil { ··· 49 49 i := 0 50 50 for { 51 51 i++ 52 - msg := messagebroker.Message{ 52 + msg := pubsub.Message{ 53 53 Topic: "topic a", 54 54 Data: []byte(fmt.Sprintf("message %d", i)), 55 55 }
+23
example/server/main.go
··· 1 + package main 2 + 3 + import ( 4 + "log" 5 + "os" 6 + "os/signal" 7 + "syscall" 8 + 9 + "github.com/willdot/messagebroker/server" 10 + ) 11 + 12 + func main() { 13 + srv, err := server.New(":3000") 14 + if err != nil { 15 + log.Fatal(err) 16 + } 17 + defer srv.Shutdown() 18 + 19 + signals := make(chan os.Signal, 1) 20 + signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) 21 + 22 + <-signals 23 + }
+5 -1
go.mod
··· 2 2 3 3 go 1.21.0 4 4 5 - require github.com/stretchr/testify v1.8.4 5 + require ( 6 + github.com/docker/distribution v2.8.3+incompatible 7 + github.com/google/uuid v1.4.0 8 + github.com/stretchr/testify v1.8.4 9 + ) 6 10 7 11 require ( 8 12 github.com/davecgh/go-spew v1.1.1 // indirect
+4
go.sum
··· 1 1 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 2 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 + github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 4 + github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 5 + github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 6 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 7 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 8 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 9 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+1 -1
message.go pubsub/message.go
··· 1 - package messagebroker 1 + package pubsub 2 2 3 3 // Message represents a message that can be published or consumed 4 4 type Message struct {
+33 -16
pubsub/publisher.go
··· 2 2 3 3 import ( 4 4 "encoding/binary" 5 - "encoding/json" 6 5 "fmt" 7 6 "net" 7 + "sync" 8 8 9 - "github.com/willdot/messagebroker" 10 9 "github.com/willdot/messagebroker/server" 11 10 ) 12 11 13 12 // Publisher allows messages to be published to a server 14 13 type Publisher struct { 15 - conn net.Conn 14 + conn net.Conn 15 + connMu sync.Mutex 16 16 } 17 17 18 18 // NewPublisher connects to the server at the given address and registers as a publisher ··· 39 39 } 40 40 41 41 // Publish will publish the given message to the server 42 - func (p *Publisher) PublishMessage(message messagebroker.Message) error { 43 - b, err := json.Marshal(message) 44 - if err != nil { 45 - return fmt.Errorf("failed to marshal message: %w", err) 42 + func (p *Publisher) PublishMessage(message Message) error { 43 + op := func(conn net.Conn) error { 44 + // send topic first 45 + topic := fmt.Sprintf("topic:%s", message.Topic) 46 + err := binary.Write(p.conn, binary.BigEndian, uint32(len(topic))) 47 + if err != nil { 48 + return fmt.Errorf("failed to write topic size to server") 49 + } 50 + 51 + _, err = p.conn.Write([]byte(topic)) 52 + if err != nil { 53 + return fmt.Errorf("failed to write topic to server") 54 + } 55 + 56 + err = binary.Write(p.conn, binary.BigEndian, uint32(len(message.Data))) 57 + if err != nil { 58 + return fmt.Errorf("failed to write message size to server") 59 + } 60 + 61 + _, err = p.conn.Write(message.Data) 62 + if err != nil { 63 + return fmt.Errorf("failed to publish data to server") 64 + } 65 + return nil 46 66 } 47 67 48 - err = binary.Write(p.conn, binary.BigEndian, uint32(len(b))) 49 - if err != nil { 50 - return fmt.Errorf("failed to write message size to server") 51 - } 68 + return p.connOperation(op) 69 + } 52 70 53 - _, err = p.conn.Write(b) 54 - if err != nil { 55 - return fmt.Errorf("failed to publish data to server") 56 - } 71 + func (p *Publisher) connOperation(op connOpp) error { 72 + p.connMu.Lock() 73 + defer p.connMu.Unlock() 57 74 58 - return nil 75 + return op(p.conn) 59 76 }
+141 -97
pubsub/subscriber.go
··· 4 4 "context" 5 5 "encoding/binary" 6 6 "encoding/json" 7 + "errors" 7 8 "fmt" 8 - "log/slog" 9 9 "net" 10 + "sync" 10 11 "time" 11 12 12 - "github.com/willdot/messagebroker" 13 13 "github.com/willdot/messagebroker/server" 14 14 ) 15 15 16 + type connOpp func(conn net.Conn) error 17 + 16 18 // Subscriber allows subscriptions to a server and the consumption of messages 17 19 type Subscriber struct { 18 - conn net.Conn 20 + conn net.Conn 21 + connMu sync.Mutex 19 22 } 20 23 21 24 // NewSubscriber will connect to the server at the given address ··· 37 40 38 41 // SubscribeToTopics will subscribe to the provided topics 39 42 func (s *Subscriber) SubscribeToTopics(topicNames []string) error { 40 - err := binary.Write(s.conn, binary.BigEndian, server.Subscribe) 41 - if err != nil { 42 - return fmt.Errorf("failed to subscribe: %w", err) 43 - } 43 + op := func(conn net.Conn) error { 44 + err := binary.Write(conn, binary.BigEndian, server.Subscribe) 45 + if err != nil { 46 + return fmt.Errorf("failed to subscribe: %w", err) 47 + } 44 48 45 - b, err := json.Marshal(topicNames) 46 - if err != nil { 47 - return fmt.Errorf("failed to marshal topic names: %w", err) 48 - } 49 + b, err := json.Marshal(topicNames) 50 + if err != nil { 51 + return fmt.Errorf("failed to marshal topic names: %w", err) 52 + } 49 53 50 - err = binary.Write(s.conn, binary.BigEndian, uint32(len(b))) 51 - if err != nil { 52 - return fmt.Errorf("failed to write topic data length: %w", err) 53 - } 54 + err = binary.Write(conn, binary.BigEndian, uint32(len(b))) 55 + if err != nil { 56 + return fmt.Errorf("failed to write topic data length: %w", err) 57 + } 54 58 55 - _, err = s.conn.Write(b) 56 - if err != nil { 57 - return fmt.Errorf("failed to subscribe to topics: %w", err) 58 - } 59 + _, err = conn.Write(b) 60 + if err != nil { 61 + return fmt.Errorf("failed to subscribe to topics: %w", err) 62 + } 59 63 60 - var resp server.Status 61 - err = binary.Read(s.conn, binary.BigEndian, &resp) 62 - if err != nil { 63 - return fmt.Errorf("failed to read confirmation of subscription: %w", err) 64 - } 64 + var resp server.Status 65 + err = binary.Read(conn, binary.BigEndian, &resp) 66 + if err != nil { 67 + return fmt.Errorf("failed to read confirmation of subscription: %w", err) 68 + } 69 + 70 + if resp == server.Subscribed { 71 + return nil 72 + } 65 73 66 - if resp == server.Subscribed { 67 - return nil 68 - } 74 + var dataLen uint32 75 + err = binary.Read(conn, binary.BigEndian, &dataLen) 76 + if err != nil { 77 + return fmt.Errorf("received status %s:", resp) 78 + } 69 79 70 - var dataLen uint32 71 - err = binary.Read(s.conn, binary.BigEndian, &dataLen) 72 - if err != nil { 73 - return fmt.Errorf("received status %s:", resp) 74 - } 80 + buf := make([]byte, dataLen) 81 + _, err = conn.Read(buf) 82 + if err != nil { 83 + return fmt.Errorf("received status %s:", resp) 84 + } 75 85 76 - buf := make([]byte, dataLen) 77 - _, err = s.conn.Read(buf) 78 - if err != nil { 79 - return fmt.Errorf("received status %s:", resp) 86 + return fmt.Errorf("received status %s - %s", resp, buf) 80 87 } 81 88 82 - return fmt.Errorf("received status %s - %s", resp, buf) 89 + return s.connOperation(op) 83 90 } 84 91 85 92 // UnsubscribeToTopics will unsubscribe to the provided topics 86 93 func (s *Subscriber) UnsubscribeToTopics(topicNames []string) error { 87 - err := binary.Write(s.conn, binary.BigEndian, server.Unsubscribe) 88 - if err != nil { 89 - return fmt.Errorf("failed to unsubscribe: %w", err) 90 - } 94 + op := func(conn net.Conn) error { 95 + err := binary.Write(conn, binary.BigEndian, server.Unsubscribe) 96 + if err != nil { 97 + return fmt.Errorf("failed to unsubscribe: %w", err) 98 + } 91 99 92 - b, err := json.Marshal(topicNames) 93 - if err != nil { 94 - return fmt.Errorf("failed to marshal topic names: %w", err) 95 - } 100 + b, err := json.Marshal(topicNames) 101 + if err != nil { 102 + return fmt.Errorf("failed to marshal topic names: %w", err) 103 + } 96 104 97 - err = binary.Write(s.conn, binary.BigEndian, uint32(len(b))) 98 - if err != nil { 99 - return fmt.Errorf("failed to write topic data length: %w", err) 100 - } 105 + err = binary.Write(conn, binary.BigEndian, uint32(len(b))) 106 + if err != nil { 107 + return fmt.Errorf("failed to write topic data length: %w", err) 108 + } 101 109 102 - _, err = s.conn.Write(b) 103 - if err != nil { 104 - return fmt.Errorf("failed to unsubscribe to topics: %w", err) 105 - } 110 + _, err = conn.Write(b) 111 + if err != nil { 112 + return fmt.Errorf("failed to unsubscribe to topics: %w", err) 113 + } 106 114 107 - var resp server.Status 108 - err = binary.Read(s.conn, binary.BigEndian, &resp) 109 - if err != nil { 110 - return fmt.Errorf("failed to read confirmation of unsubscription: %w", err) 111 - } 115 + var resp server.Status 116 + err = binary.Read(conn, binary.BigEndian, &resp) 117 + if err != nil { 118 + return fmt.Errorf("failed to read confirmation of unsubscription: %w", err) 119 + } 112 120 113 - if resp == server.Unsubscribed { 114 - return nil 115 - } 121 + if resp == server.Unsubscribed { 122 + return nil 123 + } 116 124 117 - var dataLen uint32 118 - err = binary.Read(s.conn, binary.BigEndian, &dataLen) 119 - if err != nil { 120 - return fmt.Errorf("received status %s:", resp) 121 - } 125 + var dataLen uint32 126 + err = binary.Read(conn, binary.BigEndian, &dataLen) 127 + if err != nil { 128 + return fmt.Errorf("received status %s:", resp) 129 + } 122 130 123 - buf := make([]byte, dataLen) 124 - _, err = s.conn.Read(buf) 125 - if err != nil { 126 - return fmt.Errorf("received status %s:", resp) 131 + buf := make([]byte, dataLen) 132 + _, err = conn.Read(buf) 133 + if err != nil { 134 + return fmt.Errorf("received status %s:", resp) 135 + } 136 + 137 + return fmt.Errorf("received status %s - %s", resp, buf) 127 138 } 128 139 129 - return fmt.Errorf("received status %s - %s", resp, buf) 140 + return s.connOperation(op) 130 141 } 131 142 132 143 // Consumer allows the consumption of messages. If during the consumer receiving messages from the 133 144 // server an error occurs, it will be stored in Err 134 145 type Consumer struct { 135 - msgs chan messagebroker.Message 146 + msgs chan Message 136 147 // TODO: better error handling? Maybe a channel of errors? 137 148 Err error 138 149 } 139 150 140 151 // Messages returns a channel in which this consumer will put messages onto. It is safe to range over the channel since it will be closed once 141 152 // the consumer has finished either due to an error or from being cancelled. 142 - func (c *Consumer) Messages() <-chan messagebroker.Message { 153 + func (c *Consumer) Messages() <-chan Message { 143 154 return c.msgs 144 155 } 145 156 ··· 147 158 // to read the messages 148 159 func (s *Subscriber) Consume(ctx context.Context) *Consumer { 149 160 consumer := &Consumer{ 150 - msgs: make(chan messagebroker.Message), 161 + msgs: make(chan Message), 151 162 } 152 163 153 164 go s.consume(ctx, consumer) ··· 174 185 } 175 186 } 176 187 177 - func (s *Subscriber) readMessage() (*messagebroker.Message, error) { 178 - err := s.conn.SetReadDeadline(time.Now().Add(time.Second)) 179 - if err != nil { 180 - return nil, err 188 + func (s *Subscriber) readMessage() (*Message, error) { 189 + var msg *Message 190 + op := func(conn net.Conn) error { 191 + err := s.conn.SetReadDeadline(time.Now().Add(time.Second)) 192 + if err != nil { 193 + return err 194 + } 195 + 196 + var topicLen uint64 197 + err = binary.Read(s.conn, binary.BigEndian, &topicLen) 198 + if err != nil { 199 + // TODO: check if this is needed elsewhere. I'm not sure where the read deadline resets.... 200 + if neterr, ok := err.(net.Error); ok && neterr.Timeout() { 201 + return nil 202 + } 203 + return err 204 + } 205 + 206 + topicBuf := make([]byte, topicLen) 207 + _, err = s.conn.Read(topicBuf) 208 + if err != nil { 209 + return err 210 + } 211 + 212 + var dataLen uint64 213 + err = binary.Read(s.conn, binary.BigEndian, &dataLen) 214 + if err != nil { 215 + return err 216 + } 217 + 218 + if dataLen <= 0 { 219 + return nil 220 + } 221 + 222 + dataBuf := make([]byte, dataLen) 223 + _, err = s.conn.Read(dataBuf) 224 + if err != nil { 225 + return err 226 + } 227 + 228 + msg = &Message{ 229 + Data: dataBuf, 230 + Topic: string(topicBuf), 231 + } 232 + 233 + return nil 234 + 181 235 } 182 236 183 - var dataLen uint64 184 - err = binary.Read(s.conn, binary.BigEndian, &dataLen) 237 + err := s.connOperation(op) 185 238 if err != nil { 186 - if neterr, ok := err.(net.Error); ok && neterr.Timeout() { 239 + var neterr net.Error 240 + if errors.As(err, &neterr) && neterr.Timeout() { 187 241 return nil, nil 188 242 } 189 243 return nil, err 190 244 } 191 245 192 - if dataLen <= 0 { 193 - return nil, nil 194 - } 246 + return msg, err 247 + } 195 248 196 - buf := make([]byte, dataLen) 197 - _, err = s.conn.Read(buf) 198 - if err != nil { 199 - return nil, err 200 - } 249 + func (s *Subscriber) connOperation(op connOpp) error { 250 + s.connMu.Lock() 251 + defer s.connMu.Unlock() 201 252 202 - var msg messagebroker.Message 203 - err = json.Unmarshal(buf, &msg) 204 - if err != nil { 205 - slog.Error("failed to unmarshal message", "error", err) 206 - return nil, nil 207 - } 208 - 209 - return &msg, nil 253 + return op(s.conn) 210 254 }
+19 -22
pubsub/subscriber_test.go
··· 8 8 9 9 "github.com/stretchr/testify/assert" 10 10 "github.com/stretchr/testify/require" 11 - "github.com/willdot/messagebroker" 12 11 13 12 "github.com/willdot/messagebroker/server" 14 13 ) 15 14 16 15 const ( 17 - serverAddr = ":3000" 16 + serverAddr = ":9999" 17 + topicA = "topic a" 18 + topicB = "topic b" 18 19 ) 19 20 20 21 func createServer(t *testing.T) { 21 - server, err := server.New(context.Background(), serverAddr) 22 + server, err := server.New(serverAddr) 22 23 require.NoError(t, err) 23 24 24 25 t.Cleanup(func() { ··· 72 73 sub.Close() 73 74 }) 74 75 75 - topics := []string{"topic a", "topic b"} 76 + topics := []string{topicA, topicB} 76 77 77 78 err = sub.SubscribeToTopics(topics) 78 79 require.NoError(t, err) ··· 88 89 sub.Close() 89 90 }) 90 91 91 - topics := []string{"topic a", "topic b"} 92 + topics := []string{topicA, topicB} 92 93 93 94 err = sub.SubscribeToTopics(topics) 94 95 require.NoError(t, err) 95 96 96 - err = sub.UnsubscribeToTopics([]string{"topic a"}) 97 + err = sub.UnsubscribeToTopics([]string{topicA}) 97 98 require.NoError(t, err) 98 99 99 100 ctx, cancel := context.WithCancel(context.Background()) ··· 104 105 consumer := sub.Consume(ctx) 105 106 require.NoError(t, err) 106 107 107 - var receivedMessages []messagebroker.Message 108 + var receivedMessages []Message 108 109 consumerFinCh := make(chan struct{}) 109 110 go func() { 110 111 for msg := range consumer.Messages() { ··· 118 119 // publish a message to both topics and check the subscriber only gets the message from the 1 topic 119 120 // and not the unsubscribed topic 120 121 121 - publisher, err := NewPublisher("localhost:3000") 122 + publisher, err := NewPublisher("localhost:9999") 122 123 require.NoError(t, err) 123 124 t.Cleanup(func() { 124 125 publisher.Close() 125 126 }) 126 127 127 - msg := messagebroker.Message{ 128 - Topic: "topic a", 128 + msg := Message{ 129 + Topic: topicA, 129 130 Data: []byte("hello world"), 130 131 } 131 132 132 133 err = publisher.PublishMessage(msg) 133 134 require.NoError(t, err) 134 135 135 - msg.Topic = "topic b" 136 + msg.Topic = topicB 136 137 err = publisher.PublishMessage(msg) 137 138 require.NoError(t, err) 138 139 139 140 cancel() 140 141 141 - // give the consumer some time to read the messages -- TODO: make better! 142 - time.Sleep(time.Millisecond * 500) 143 - cancel() 144 - 145 142 select { 146 143 case <-consumerFinCh: 147 144 break ··· 150 147 } 151 148 152 149 assert.Len(t, receivedMessages, 1) 153 - assert.Equal(t, "topic b", receivedMessages[0].Topic) 150 + assert.Equal(t, topicB, receivedMessages[0].Topic) 154 151 } 155 152 156 153 func TestPublishAndSubscribe(t *testing.T) { ··· 163 160 sub.Close() 164 161 }) 165 162 166 - topics := []string{"topic a", "topic b"} 163 + topics := []string{topicA, topicB} 167 164 168 165 err = sub.SubscribeToTopics(topics) 169 166 require.NoError(t, err) ··· 176 173 consumer := sub.Consume(ctx) 177 174 require.NoError(t, err) 178 175 179 - var receivedMessages []messagebroker.Message 176 + var receivedMessages []Message 180 177 181 178 consumerFinCh := make(chan struct{}) 182 179 go func() { ··· 188 185 consumerFinCh <- struct{}{} 189 186 }() 190 187 191 - publisher, err := NewPublisher("localhost:3000") 188 + publisher, err := NewPublisher("localhost:9999") 192 189 require.NoError(t, err) 193 190 t.Cleanup(func() { 194 191 publisher.Close() 195 192 }) 196 193 197 194 // send some messages 198 - sentMessages := make([]messagebroker.Message, 0, 10) 195 + sentMessages := make([]Message, 0, 10) 199 196 for i := 0; i < 10; i++ { 200 - msg := messagebroker.Message{ 201 - Topic: "topic a", 197 + msg := Message{ 198 + Topic: topicA, 202 199 Data: []byte(fmt.Sprintf("message %d", i)), 203 200 } 204 201
+28 -65
server/peer.go
··· 1 1 package server 2 2 3 3 import ( 4 - "encoding/binary" 5 - "fmt" 6 4 "log/slog" 7 5 "net" 8 - ) 9 - 10 - type peer struct { 11 - conn net.Conn 12 - } 13 - 14 - func newPeer(conn net.Conn) peer { 15 - return peer{ 16 - conn: conn, 17 - } 18 - } 6 + "sync" 19 7 20 - // Read wraps the peers underlying connections Read function to satisfy io.Reader 21 - func (p *peer) Read(b []byte) (n int, err error) { 22 - return p.conn.Read(b) 23 - } 24 - 25 - // Write wraps the peers underlying connections Write function to satisfy io.Writer 26 - func (p *peer) Write(b []byte) (n int, err error) { 27 - return p.conn.Write(b) 28 - } 29 - 30 - func (p *peer) addr() net.Addr { 31 - return p.conn.LocalAddr() 32 - } 33 - 34 - func (p *peer) readAction() (Action, error) { 35 - var action Action 36 - err := binary.Read(p.conn, binary.BigEndian, &action) 37 - if err != nil { 38 - return 0, fmt.Errorf("failed to read action from peer: %w", err) 39 - } 40 - 41 - return action, nil 42 - } 43 - 44 - func (p *peer) readDataLength() (uint32, error) { 45 - var dataLen uint32 46 - err := binary.Read(p.conn, binary.BigEndian, &dataLen) 47 - if err != nil { 48 - return 0, fmt.Errorf("failed to read data length from peer: %w", err) 49 - } 50 - 51 - return dataLen, nil 52 - } 8 + "github.com/google/uuid" 9 + ) 53 10 54 11 // Status represents the status of a request 55 12 type Status uint8 ··· 73 30 return "" 74 31 } 75 32 76 - func (p *peer) writeStatus(status Status, message string) { 77 - err := binary.Write(p.conn, binary.BigEndian, status) 78 - if err != nil { 79 - slog.Error("failed to write status to peers connection", "error", err, "peer", p.addr()) 80 - return 81 - } 33 + type peer struct { 34 + conn net.Conn 35 + connMu sync.Mutex 36 + name string 37 + } 82 38 83 - if message == "" { 84 - return 39 + func newPeer(conn net.Conn) peer { 40 + return peer{ 41 + conn: conn, 42 + name: uuid.New().String(), 85 43 } 44 + } 86 45 87 - msgBytes := []byte(message) 88 - err = binary.Write(p.conn, binary.BigEndian, uint32(len(msgBytes))) 89 - if err != nil { 90 - slog.Error("failed to write message length to peers connection", "error", err, "peer", p.addr()) 91 - return 92 - } 46 + func (p *peer) addr() net.Addr { 47 + return p.conn.RemoteAddr() 48 + } 49 + 50 + type connOpp func(conn net.Conn) error 51 + 52 + func (p *peer) connOperation(op connOpp, from string) error { 53 + slog.Info("operation running", "from", from, "peer", p.conn.RemoteAddr(), "name", p.name, "mu addr", &p.connMu) 54 + 55 + p.connMu.Lock() 56 + err := op(p.conn) 57 + p.connMu.Unlock() 93 58 94 - _, err = p.conn.Write(msgBytes) 95 - if err != nil { 96 - slog.Error("failed to write message to peers connection", "error", err, "peer", p.addr()) 97 - return 98 - } 59 + slog.Info("operation finished", "from", from, "peer", p.conn.RemoteAddr(), "name", p.name, "mu addr", &p.connMu) 60 + 61 + return err 99 62 }
+199 -88
server/server.go
··· 1 1 package server 2 2 3 3 import ( 4 - "context" 4 + "encoding/binary" 5 5 "encoding/json" 6 6 "errors" 7 7 "fmt" 8 8 "log/slog" 9 9 "net" 10 + "strings" 10 11 "sync" 11 - 12 - "github.com/willdot/messagebroker" 12 + "time" 13 13 ) 14 14 15 15 // Action represents the type of action that a peer requests to do ··· 31 31 } 32 32 33 33 // New creates and starts a new server 34 - func New(ctx context.Context, addr string) (*Server, error) { 34 + func New(addr string) (*Server, error) { 35 35 lis, err := net.Listen("tcp", addr) 36 36 if err != nil { 37 37 return nil, fmt.Errorf("failed to listen: %w", err) ··· 42 42 topics: map[string]topic{}, 43 43 } 44 44 45 - go srv.start(ctx) 45 + go srv.start() 46 46 47 47 return srv, nil 48 48 } ··· 52 52 return s.lis.Close() 53 53 } 54 54 55 - func (s *Server) start(ctx context.Context) { 55 + func (s *Server) start() { 56 56 for { 57 57 conn, err := s.lis.Accept() 58 58 if err != nil { ··· 70 70 71 71 func (s *Server) handleConn(conn net.Conn) { 72 72 peer := newPeer(conn) 73 - action, err := peer.readAction() 73 + action, err := readAction(peer) 74 74 if err != nil { 75 75 slog.Error("failed to read action from peer", "error", err, "peer", peer.addr()) 76 76 return ··· 85 85 s.handlePublish(peer) 86 86 default: 87 87 slog.Error("unknown action", "action", action, "peer", peer.addr()) 88 - peer.writeStatus(Error, "unknown action") 88 + writeStatus(Error, "unknown action", peer.conn) 89 89 } 90 90 } 91 91 92 92 func (s *Server) handleSubscribe(peer peer) { 93 93 // subscribe the peer to the topic 94 - s.subscribePeerToTopic(peer) 94 + s.subscribePeerToTopic(&peer) 95 95 96 96 // keep handling the peers connection, getting the action from the peer when it wishes to do something else. 97 97 // once the peers connection ends, it will be unsubscribed from all topics and returned 98 98 for { 99 - action, err := peer.readAction() 99 + action, err := readAction(peer) 100 100 if err != nil { 101 + var neterr net.Error 102 + if errors.As(err, &neterr) && neterr.Timeout() { 103 + time.Sleep(time.Second) 104 + continue 105 + } 101 106 // TODO: see if there's a way to check if the peers connection has been ended etc 102 107 slog.Error("failed to read action from subscriber", "error", err, "peer", peer.addr()) 103 108 ··· 108 113 109 114 switch action { 110 115 case Subscribe: 111 - s.subscribePeerToTopic(peer) 116 + s.subscribePeerToTopic(&peer) 112 117 case Unsubscribe: 113 118 s.handleUnsubscribe(peer) 114 119 default: 115 120 slog.Error("unknown action for subscriber", "action", action, "peer", peer.addr()) 116 - peer.writeStatus(Error, "unknown action") 121 + writeStatus(Error, "unknown action", peer.conn) 117 122 continue 118 123 } 119 124 } 120 125 } 121 126 122 - func (s *Server) subscribePeerToTopic(peer peer) { 123 - // get the topics the peer wishes to subscribe to 124 - dataLen, err := peer.readDataLength() 125 - if err != nil { 126 - slog.Error(err.Error(), "peer", peer.addr()) 127 - peer.writeStatus(Error, "invalid data length of topics provided") 128 - return 129 - } 130 - if dataLen == 0 { 131 - peer.writeStatus(Error, "data length of topics is 0") 132 - return 133 - } 134 - 135 - buf := make([]byte, dataLen) 136 - _, err = peer.Read(buf) 137 - if err != nil { 138 - slog.Error("failed to read subscibers topic data", "error", err, "peer", peer.addr()) 139 - peer.writeStatus(Error, "failed to read topic data") 140 - return 141 - } 142 - 143 - var topics []string 144 - err = json.Unmarshal(buf, &topics) 145 - if err != nil { 146 - slog.Error("failed to unmarshal subscibers topic data", "error", err, "peer", peer.addr()) 147 - peer.writeStatus(Error, "invalid topic data provided") 148 - return 149 - } 127 + func (s *Server) subscribePeerToTopic(peer *peer) { 128 + op := func(conn net.Conn) error { 129 + // get the topics the peer wishes to subscribe to 130 + dataLen, err := dataLength(conn) 131 + if err != nil { 132 + slog.Error(err.Error(), "peer", peer.addr()) 133 + writeStatus(Error, "invalid data length of topics provided", conn) 134 + return nil 135 + } 136 + if dataLen == 0 { 137 + writeStatus(Error, "data length of topics is 0", conn) 138 + return nil 139 + } 150 140 151 - s.subscribeToTopics(peer, topics) 152 - peer.writeStatus(Subscribed, "") 153 - } 141 + buf := make([]byte, dataLen) 142 + _, err = conn.Read(buf) 143 + if err != nil { 144 + slog.Error("failed to read subscibers topic data", "error", err, "peer", peer.addr()) 145 + writeStatus(Error, "failed to read topic data", conn) 146 + return nil 147 + } 154 148 155 - func (s *Server) handleUnsubscribe(peer peer) { 156 - // get the topics the peer wishes to unsubscribe from 157 - dataLen, err := peer.readDataLength() 158 - if err != nil { 159 - slog.Error(err.Error(), "peer", peer.addr()) 160 - peer.writeStatus(Error, "invalid data length of topics provided") 161 - return 162 - } 163 - if dataLen == 0 { 164 - peer.writeStatus(Error, "data length of topics is 0") 165 - return 166 - } 149 + var topics []string 150 + err = json.Unmarshal(buf, &topics) 151 + if err != nil { 152 + slog.Error("failed to unmarshal subscibers topic data", "error", err, "peer", peer.addr()) 153 + writeStatus(Error, "invalid topic data provided", conn) 154 + return nil 155 + } 167 156 168 - buf := make([]byte, dataLen) 169 - _, err = peer.Read(buf) 170 - if err != nil { 171 - slog.Error("failed to read subscibers topic data", "error", err, "peer", peer.addr()) 172 - peer.writeStatus(Error, "failed to read topic data") 173 - return 174 - } 157 + s.subscribeToTopics(peer, topics) 158 + writeStatus(Subscribed, "", conn) 175 159 176 - var topics []string 177 - err = json.Unmarshal(buf, &topics) 178 - if err != nil { 179 - slog.Error("failed to unmarshal subscibers topic data", "error", err, "peer", peer.addr()) 180 - peer.writeStatus(Error, "invalid topic data provided") 181 - return 160 + return nil 182 161 } 183 162 184 - s.unsubscribeToTopics(peer, topics) 185 - peer.writeStatus(Unsubscribed, "") 163 + _ = peer.connOperation(op, "subscribe peer to topic") 186 164 } 187 165 188 - func (s *Server) handlePublish(peer peer) { 189 - for { 190 - dataLen, err := peer.readDataLength() 166 + func (s *Server) handleUnsubscribe(peer peer) { 167 + op := func(conn net.Conn) error { 168 + // get the topics the peer wishes to unsubscribe from 169 + dataLen, err := dataLength(conn) 191 170 if err != nil { 192 171 slog.Error(err.Error(), "peer", peer.addr()) 193 - peer.writeStatus(Error, "invalid data length of data provided") 194 - return 172 + writeStatus(Error, "invalid data length of topics provided", conn) 173 + return nil 195 174 } 196 175 if dataLen == 0 { 197 - continue 176 + writeStatus(Error, "data length of topics is 0", conn) 177 + return nil 198 178 } 199 179 200 180 buf := make([]byte, dataLen) 201 - _, err = peer.Read(buf) 181 + _, err = conn.Read(buf) 202 182 if err != nil { 203 - slog.Error("failed to read data from peer", "error", err, "peer", peer.addr()) 204 - peer.writeStatus(Error, "failed to read data") 205 - return 183 + slog.Error("failed to read subscibers topic data", "error", err, "peer", peer.addr()) 184 + writeStatus(Error, "failed to read topic data", conn) 185 + return nil 206 186 } 207 187 208 - var msg messagebroker.Message 209 - err = json.Unmarshal(buf, &msg) 188 + var topics []string 189 + err = json.Unmarshal(buf, &topics) 210 190 if err != nil { 211 - slog.Error("failed to unmarshal data to message", "error", err, "peer", peer.addr()) 212 - peer.writeStatus(Error, "invalid message") 191 + slog.Error("failed to unmarshal subscibers topic data", "error", err, "peer", peer.addr()) 192 + writeStatus(Error, "invalid topic data provided", conn) 193 + return nil 194 + } 195 + 196 + s.unsubscribeToTopics(peer, topics) 197 + writeStatus(Unsubscribed, "", conn) 198 + 199 + return nil 200 + } 201 + 202 + _ = peer.connOperation(op, "handle unsubscribe") 203 + } 204 + 205 + type messageToSend struct { 206 + topic string 207 + data []byte 208 + } 209 + 210 + func (s *Server) handlePublish(peer peer) { 211 + for { 212 + var message *messageToSend 213 + 214 + op := func(conn net.Conn) error { 215 + dataLen, err := dataLength(conn) 216 + if err != nil { 217 + slog.Error(err.Error(), "peer", peer.addr()) 218 + writeStatus(Error, "invalid data length of data provided", conn) 219 + return nil 220 + } 221 + if dataLen == 0 { 222 + return nil 223 + } 224 + topicBuf := make([]byte, dataLen) 225 + _, err = conn.Read(topicBuf) 226 + if err != nil { 227 + slog.Error("failed to read topic from peer", "error", err, "peer", peer.addr()) 228 + writeStatus(Error, "failed to read topic", conn) 229 + return nil 230 + } 231 + 232 + topicStr := string(topicBuf) 233 + if !strings.HasPrefix(topicStr, "topic:") { 234 + slog.Error("topic data does not contain topic prefix", "peer", peer.addr()) 235 + writeStatus(Error, "topic data does not contain 'topic:' prefix", conn) 236 + return nil 237 + } 238 + topicStr = strings.TrimPrefix(topicStr, "topic:") 239 + 240 + dataLen, err = dataLength(conn) 241 + if err != nil { 242 + slog.Error(err.Error(), "peer", peer.addr()) 243 + writeStatus(Error, "invalid data length of data provided", conn) 244 + return nil 245 + } 246 + if dataLen == 0 { 247 + return nil 248 + } 249 + 250 + dataBuf := make([]byte, dataLen) 251 + _, err = conn.Read(dataBuf) 252 + if err != nil { 253 + slog.Error("failed to read data from peer", "error", err, "peer", peer.addr()) 254 + writeStatus(Error, "failed to read data", conn) 255 + return nil 256 + } 257 + 258 + message = &messageToSend{ 259 + topic: topicStr, 260 + data: dataBuf, 261 + } 262 + return nil 263 + } 264 + 265 + _ = peer.connOperation(op, "handle publish") 266 + 267 + if message == nil { 213 268 continue 214 269 } 270 + // TODO: this can be done in a go routine because once we've got the message from the publisher, the publisher 271 + // doesn't need to wait for us to send the message to all peers 215 272 216 - topic := s.getTopic(msg.Topic) 273 + topic := s.getTopic(message.topic) 217 274 if topic != nil { 218 - topic.sendMessageToSubscribers(msg) 275 + topic.sendMessageToSubscribers(message.data) 219 276 } 220 277 } 221 278 } 222 279 223 - func (s *Server) subscribeToTopics(peer peer, topics []string) { 280 + func (s *Server) subscribeToTopics(peer *peer, topics []string) { 224 281 for _, topic := range topics { 225 282 s.addSubsciberToTopic(topic, peer) 226 283 } 227 284 } 228 285 229 - func (s *Server) addSubsciberToTopic(topicName string, peer peer) { 286 + func (s *Server) addSubsciberToTopic(topicName string, peer *peer) { 230 287 s.mu.Lock() 231 288 defer s.mu.Unlock() 232 289 ··· 280 337 281 338 return nil 282 339 } 340 + 341 + func readAction(peer peer) (Action, error) { 342 + var action Action 343 + op := func(conn net.Conn) error { 344 + conn.SetReadDeadline(time.Now().Add(time.Second)) 345 + 346 + err := binary.Read(conn, binary.BigEndian, &action) 347 + if err != nil { 348 + return err 349 + } 350 + return nil 351 + } 352 + 353 + err := peer.connOperation(op, "read action") 354 + if err != nil { 355 + return 0, fmt.Errorf("failed to read action from peer: %w", err) 356 + } 357 + 358 + return action, nil 359 + } 360 + 361 + func dataLength(conn net.Conn) (uint32, error) { 362 + var dataLen uint32 363 + err := binary.Read(conn, binary.BigEndian, &dataLen) 364 + if err != nil { 365 + return 0, err 366 + } 367 + return dataLen, nil 368 + } 369 + 370 + func writeStatus(status Status, message string, conn net.Conn) { 371 + err := binary.Write(conn, binary.BigEndian, status) 372 + if err != nil { 373 + slog.Error("failed to write status to peers connection", "error", err, "peer", conn.RemoteAddr()) 374 + return 375 + } 376 + 377 + if message == "" { 378 + return 379 + } 380 + 381 + msgBytes := []byte(message) 382 + err = binary.Write(conn, binary.BigEndian, uint32(len(msgBytes))) 383 + if err != nil { 384 + slog.Error("failed to write message length to peers connection", "error", err, "peer", conn.RemoteAddr()) 385 + return 386 + } 387 + 388 + _, err = conn.Write(msgBytes) 389 + if err != nil { 390 + slog.Error("failed to write message to peers connection", "error", err, "peer", conn.RemoteAddr()) 391 + return 392 + } 393 + }
+82 -62
server/server_test.go
··· 1 1 package server 2 2 3 3 import ( 4 - "context" 5 4 "encoding/binary" 6 5 "encoding/json" 7 6 "fmt" ··· 11 10 12 11 "github.com/stretchr/testify/assert" 13 12 "github.com/stretchr/testify/require" 14 - "github.com/willdot/messagebroker" 13 + ) 14 + 15 + const ( 16 + topicA = "topic a" 17 + topicB = "topic b" 18 + topicC = "topic c" 19 + 20 + serverAddr = ":6666" 15 21 ) 16 22 17 23 func createServer(t *testing.T) *Server { 18 - srv, err := New(context.Background(), ":3000") 24 + srv, err := New(serverAddr) 19 25 require.NoError(t, err) 20 26 21 27 t.Cleanup(func() { ··· 36 42 } 37 43 38 44 func createConnectionAndSubscribe(t *testing.T, topics []string) net.Conn { 39 - conn, err := net.Dial("tcp", "localhost:3000") 45 + conn, err := net.Dial("tcp", fmt.Sprintf("localhost%s", serverAddr)) 40 46 require.NoError(t, err) 41 47 42 48 err = binary.Write(conn, binary.BigEndian, Subscribe) ··· 64 70 func TestSubscribeToTopics(t *testing.T) { 65 71 // create a server with an existing topic so we can test subscribing to a new and 66 72 // existing topic 67 - srv := createServerWithExistingTopic(t, "topic a") 73 + srv := createServerWithExistingTopic(t, topicA) 68 74 69 - _ = createConnectionAndSubscribe(t, []string{"topic a", "topic b"}) 75 + _ = createConnectionAndSubscribe(t, []string{topicA, topicB}) 70 76 71 77 assert.Len(t, srv.topics, 2) 72 - assert.Len(t, srv.topics["topic a"].subscriptions, 1) 73 - assert.Len(t, srv.topics["topic b"].subscriptions, 1) 78 + assert.Len(t, srv.topics[topicA].subscriptions, 1) 79 + assert.Len(t, srv.topics[topicB].subscriptions, 1) 74 80 } 75 81 76 82 func TestUnsubscribesFromTopic(t *testing.T) { 77 - srv := createServerWithExistingTopic(t, "topic a") 83 + srv := createServerWithExistingTopic(t, topicA) 78 84 79 - conn := createConnectionAndSubscribe(t, []string{"topic a", "topic b", "topic c"}) 85 + conn := createConnectionAndSubscribe(t, []string{topicA, topicB, topicC}) 80 86 81 87 assert.Len(t, srv.topics, 3) 82 - assert.Len(t, srv.topics["topic a"].subscriptions, 1) 83 - assert.Len(t, srv.topics["topic b"].subscriptions, 1) 84 - assert.Len(t, srv.topics["topic c"].subscriptions, 1) 88 + assert.Len(t, srv.topics[topicA].subscriptions, 1) 89 + assert.Len(t, srv.topics[topicB].subscriptions, 1) 90 + assert.Len(t, srv.topics[topicC].subscriptions, 1) 85 91 86 92 err := binary.Write(conn, binary.BigEndian, Unsubscribe) 87 93 require.NoError(t, err) 88 94 89 - topics := []string{"topic a", "topic b"} 95 + topics := []string{topicA, topicB} 90 96 rawTopics, err := json.Marshal(topics) 91 97 require.NoError(t, err) 92 98 ··· 104 110 assert.Equal(t, expectedRes, int(resp)) 105 111 106 112 assert.Len(t, srv.topics, 3) 107 - assert.Len(t, srv.topics["topic a"].subscriptions, 0) 108 - assert.Len(t, srv.topics["topic b"].subscriptions, 0) 109 - assert.Len(t, srv.topics["topic c"].subscriptions, 1) 113 + assert.Len(t, srv.topics[topicA].subscriptions, 0) 114 + assert.Len(t, srv.topics[topicB].subscriptions, 0) 115 + assert.Len(t, srv.topics[topicC].subscriptions, 1) 110 116 } 111 117 112 118 func TestSubscriberClosesWithoutUnsubscribing(t *testing.T) { 113 119 srv := createServer(t) 114 120 115 - conn := createConnectionAndSubscribe(t, []string{"topic a", "topic b"}) 121 + conn := createConnectionAndSubscribe(t, []string{topicA, topicB}) 116 122 117 123 assert.Len(t, srv.topics, 2) 118 - assert.Len(t, srv.topics["topic a"].subscriptions, 1) 119 - assert.Len(t, srv.topics["topic b"].subscriptions, 1) 124 + assert.Len(t, srv.topics[topicA].subscriptions, 1) 125 + assert.Len(t, srv.topics[topicB].subscriptions, 1) 120 126 121 127 // close the conn 122 128 err := conn.Close() 123 129 require.NoError(t, err) 124 130 125 - publisherConn, err := net.Dial("tcp", "localhost:3000") 131 + publisherConn, err := net.Dial("tcp", fmt.Sprintf("localhost%s", serverAddr)) 126 132 require.NoError(t, err) 127 133 128 134 err = binary.Write(publisherConn, binary.BigEndian, Publish) ··· 137 143 require.Equal(t, len(data), n) 138 144 139 145 assert.Len(t, srv.topics, 2) 140 - assert.Len(t, srv.topics["topic a"].subscriptions, 0) 141 - assert.Len(t, srv.topics["topic b"].subscriptions, 0) 146 + assert.Len(t, srv.topics[topicA].subscriptions, 0) 147 + assert.Len(t, srv.topics[topicB].subscriptions, 0) 142 148 } 143 149 144 150 func TestInvalidAction(t *testing.T) { 145 151 _ = createServer(t) 146 152 147 - conn, err := net.Dial("tcp", "localhost:3000") 153 + conn, err := net.Dial("tcp", fmt.Sprintf("localhost%s", serverAddr)) 148 154 require.NoError(t, err) 149 155 150 156 err = binary.Write(conn, binary.BigEndian, uint8(99)) ··· 170 176 assert.Equal(t, expectedMessage, string(buf)) 171 177 } 172 178 173 - func TestInvalidMessagePublished(t *testing.T) { 179 + func TestInvalidTopicDataPublished(t *testing.T) { 174 180 _ = createServer(t) 175 181 176 - publisherConn, err := net.Dial("tcp", "localhost:3000") 182 + publisherConn, err := net.Dial("tcp", fmt.Sprintf("localhost%s", serverAddr)) 177 183 require.NoError(t, err) 178 184 179 185 err = binary.Write(publisherConn, binary.BigEndian, Publish) 180 186 require.NoError(t, err) 181 187 182 - // send some data 183 - data := []byte("this isn't wrapped in a message type") 184 - 185 - // send data length first 186 - err = binary.Write(publisherConn, binary.BigEndian, uint32(len(data))) 188 + // send topic 189 + topic := topicA 190 + err = binary.Write(publisherConn, binary.BigEndian, uint32(len(topic))) 187 191 require.NoError(t, err) 188 - n, err := publisherConn.Write(data) 192 + _, err = publisherConn.Write([]byte(topic)) 189 193 require.NoError(t, err) 190 - require.Equal(t, len(data), n) 191 194 192 195 expectedRes := Error 193 196 ··· 196 199 197 200 assert.Equal(t, expectedRes, int(resp)) 198 201 199 - expectedMessage := "invalid message" 202 + expectedMessage := "topic data does not contain 'topic:' prefix" 200 203 201 204 var dataLen uint32 202 205 err = binary.Read(publisherConn, binary.BigEndian, &dataLen) ··· 212 215 func TestSendsDataToTopicSubscribers(t *testing.T) { 213 216 _ = createServer(t) 214 217 215 - subscribers := make([]net.Conn, 0, 5) 216 - for i := 0; i < 5; i++ { 217 - subscriberConn := createConnectionAndSubscribe(t, []string{"topic a", "topic b"}) 218 + subscribers := make([]net.Conn, 0, 1) 219 + for i := 0; i < 1; i++ { 220 + subscriberConn := createConnectionAndSubscribe(t, []string{topicA, topicB}) 218 221 219 222 subscribers = append(subscribers, subscriberConn) 220 223 } 221 224 222 - publisherConn, err := net.Dial("tcp", "localhost:3000") 225 + publisherConn, err := net.Dial("tcp", fmt.Sprintf("localhost%s", serverAddr)) 223 226 require.NoError(t, err) 224 227 225 228 err = binary.Write(publisherConn, binary.BigEndian, Publish) 226 229 require.NoError(t, err) 227 230 228 - // send a message 229 - msg := messagebroker.Message{ 230 - Topic: "topic a", 231 - Data: []byte("hello world"), 232 - } 231 + topic := fmt.Sprintf("topic:%s", topicA) 232 + messageData := "hello world" 233 233 234 - rawMsg, err := json.Marshal(msg) 234 + // send topic first 235 + err = binary.Write(publisherConn, binary.BigEndian, uint32(len(topic))) 236 + require.NoError(t, err) 237 + _, err = publisherConn.Write([]byte(topic)) 235 238 require.NoError(t, err) 236 239 237 - // send data length first 238 - err = binary.Write(publisherConn, binary.BigEndian, uint32(len(rawMsg))) 240 + // now send the data 241 + err = binary.Write(publisherConn, binary.BigEndian, uint32(len(messageData))) 239 242 require.NoError(t, err) 240 - n, err := publisherConn.Write(rawMsg) 243 + n, err := publisherConn.Write([]byte(messageData)) 241 244 require.NoError(t, err) 242 - require.Equal(t, len(rawMsg), n) 245 + require.Equal(t, len(messageData), n) 243 246 244 247 // check the subsribers got the data 245 248 for _, conn := range subscribers { 249 + var topicLen uint64 250 + err = binary.Read(conn, binary.BigEndian, &topicLen) 251 + require.NoError(t, err) 252 + 253 + topicBuf := make([]byte, topicLen) 254 + _, err = conn.Read(topicBuf) 255 + require.NoError(t, err) 256 + assert.Equal(t, topicA, string(topicBuf)) 246 257 247 258 var dataLen uint64 248 259 err = binary.Read(conn, binary.BigEndian, &dataLen) ··· 253 264 require.NoError(t, err) 254 265 require.Equal(t, int(dataLen), n) 255 266 256 - assert.Equal(t, rawMsg, buf) 267 + assert.Equal(t, messageData, string(buf)) 257 268 } 258 269 } 259 270 260 271 func TestPublishMultipleTimes(t *testing.T) { 261 272 _ = createServer(t) 262 273 263 - publisherConn, err := net.Dial("tcp", "localhost:3000") 274 + publisherConn, err := net.Dial("tcp", fmt.Sprintf("localhost%s", serverAddr)) 264 275 require.NoError(t, err) 265 276 266 277 err = binary.Write(publisherConn, binary.BigEndian, Publish) ··· 268 279 269 280 messages := make([][]byte, 0, 10) 270 281 for i := 0; i < 10; i++ { 271 - msg := messagebroker.Message{ 272 - Topic: "topic a", 273 - Data: []byte(fmt.Sprintf("message %d", i)), 274 - } 275 - 276 - rawMsg, err := json.Marshal(msg) 277 - require.NoError(t, err) 278 - 279 - messages = append(messages, rawMsg) 282 + messages = append(messages, []byte(fmt.Sprintf("message %d", i))) 280 283 } 281 284 282 285 subscribeFinCh := make(chan struct{}) 283 286 // create a subscriber that will read messages 284 - subscriberConn := createConnectionAndSubscribe(t, []string{"topic a", "topic b"}) 287 + subscriberConn := createConnectionAndSubscribe(t, []string{topicA, topicB}) 285 288 go func() { 286 289 // check subscriber got all messages 287 290 for _, msg := range messages { 291 + var topicLen uint64 292 + err = binary.Read(subscriberConn, binary.BigEndian, &topicLen) 293 + require.NoError(t, err) 294 + 295 + topicBuf := make([]byte, topicLen) 296 + _, err = subscriberConn.Read(topicBuf) 297 + require.NoError(t, err) 298 + assert.Equal(t, topicA, string(topicBuf)) 299 + 288 300 var dataLen uint64 289 301 err = binary.Read(subscriberConn, binary.BigEndian, &dataLen) 290 302 require.NoError(t, err) ··· 300 312 subscribeFinCh <- struct{}{} 301 313 }() 302 314 315 + topic := fmt.Sprintf("topic:%s", topicA) 316 + 303 317 // send multiple messages 304 318 for _, msg := range messages { 305 - // send data length first 319 + // send topic first 320 + err = binary.Write(publisherConn, binary.BigEndian, uint32(len(topic))) 321 + require.NoError(t, err) 322 + _, err = publisherConn.Write([]byte(topic)) 323 + require.NoError(t, err) 324 + 325 + // now send the data 306 326 err = binary.Write(publisherConn, binary.BigEndian, uint32(len(msg))) 307 327 require.NoError(t, err) 308 - n, err := publisherConn.Write(msg) 328 + n, err := publisherConn.Write([]byte(msg)) 309 329 require.NoError(t, err) 310 330 require.Equal(t, len(msg), n) 311 331 }
-26
server/subscriber.go
··· 1 - package server 2 - 3 - import ( 4 - "encoding/binary" 5 - "fmt" 6 - ) 7 - 8 - type subscriber struct { 9 - peer peer 10 - currentOffset int 11 - } 12 - 13 - func (s *subscriber) sendMessage(msg []byte) error { 14 - dataLen := uint64(len(msg)) 15 - 16 - err := binary.Write(&s.peer, binary.BigEndian, dataLen) 17 - if err != nil { 18 - return fmt.Errorf("failed to send data length: %w", err) 19 - } 20 - 21 - _, err = s.peer.Write(msg) 22 - if err != nil { 23 - return fmt.Errorf("failed to write to peer: %w", err) 24 - } 25 - return nil 26 - }
+39 -11
server/topic.go
··· 1 1 package server 2 2 3 3 import ( 4 - "encoding/json" 4 + "encoding/binary" 5 + "fmt" 5 6 "log/slog" 6 7 "net" 7 8 "sync" 8 - 9 - "github.com/willdot/messagebroker" 10 9 ) 11 10 12 11 type topic struct { ··· 15 14 mu sync.Mutex 16 15 } 17 16 17 + type subscriber struct { 18 + peer *peer 19 + currentOffset int 20 + } 21 + 18 22 func newTopic(name string) topic { 19 23 return topic{ 20 24 name: name, ··· 30 34 delete(t.subscriptions, addr) 31 35 } 32 36 33 - func (t *topic) sendMessageToSubscribers(msg messagebroker.Message) { 37 + func (t *topic) sendMessageToSubscribers(msgData []byte) { 34 38 t.mu.Lock() 35 39 subscribers := t.subscriptions 36 40 t.mu.Unlock() 37 41 38 - msgData, err := json.Marshal(msg) 39 - if err != nil { 40 - slog.Error("failed to marshal message for subscribers", "error", err) 41 - } 42 + for addr, subscriber := range subscribers { 43 + //sendMessageOpFunc := sendMessageOp(t.name, msgData) 42 44 43 - for addr, subscriber := range subscribers { 44 - err := subscriber.sendMessage(msgData) 45 + err := subscriber.peer.connOperation(sendMessageOp(t.name, msgData), "send message to subscribers") 45 46 if err != nil { 46 47 slog.Error("failed to send to message", "error", err, "peer", addr) 47 - continue 48 + return 48 49 } 49 50 } 50 51 } 52 + 53 + func sendMessageOp(topic string, data []byte) connOpp { 54 + return func(conn net.Conn) error { 55 + topicLen := uint64(len(topic)) 56 + err := binary.Write(conn, binary.BigEndian, topicLen) 57 + if err != nil { 58 + return fmt.Errorf("failed to send topic length: %w", err) 59 + } 60 + _, err = conn.Write([]byte(topic)) 61 + if err != nil { 62 + return fmt.Errorf("failed to send topic: %w", err) 63 + } 64 + 65 + dataLen := uint64(len(data)) 66 + 67 + err = binary.Write(conn, binary.BigEndian, dataLen) 68 + if err != nil { 69 + return fmt.Errorf("failed to send data length: %w", err) 70 + } 71 + 72 + _, err = conn.Write(data) 73 + if err != nil { 74 + return fmt.Errorf("failed to write to peer: %w", err) 75 + } 76 + return nil 77 + } 78 + }