github.com/vmware/transport-go@v1.3.4/bridge/connection.go (about) 1 // Copyright 2019-2020 VMware, Inc. 2 // SPDX-License-Identifier: BSD-2-Clause 3 4 package bridge 5 6 import ( 7 "fmt" 8 "github.com/go-stomp/stomp/v3" 9 "github.com/go-stomp/stomp/v3/frame" 10 "github.com/google/uuid" 11 "github.com/vmware/transport-go/model" 12 "log" 13 "sync" 14 ) 15 16 type Connection interface { 17 GetId() *uuid.UUID 18 Subscribe(destination string) (Subscription, error) 19 SubscribeReplyDestination(destination string) (Subscription, error) 20 Disconnect() (err error) 21 SendJSONMessage(destination string, payload []byte, opts ...func(*frame.Frame) error) error 22 SendMessage(destination, contentType string, payload []byte, opts ...func(*frame.Frame) error) error 23 SendMessageWithReplyDestination(destination, replyDestination, contentType string, payload []byte, opts ...func(*frame.Frame) error) error 24 } 25 26 // Connection represents a Connection to a message broker. 27 type connection struct { 28 id *uuid.UUID 29 useWs bool 30 conn *stomp.Conn 31 wsConn *BridgeClient 32 disconnectChan chan bool 33 subscriptions map[string]Subscription 34 connLock sync.Mutex 35 } 36 37 func (c *connection) GetId() *uuid.UUID { 38 return c.id 39 } 40 41 // Subscribe to a destination, only one subscription can exist for a destination 42 func (c *connection) Subscribe(destination string) (Subscription, error) { 43 // check if the subscription exists, if so, return it. 44 if c == nil { 45 return nil, fmt.Errorf("cannot subscribe to '%s', no connection to broker", destination) 46 } 47 48 c.connLock.Lock() 49 if sub, ok := c.subscriptions[destination]; ok { 50 c.connLock.Unlock() 51 return sub, nil 52 } 53 c.connLock.Unlock() 54 55 // use websocket, if not use stomp TCP. 56 if c.useWs { 57 return c.subscribeWs(destination) 58 } 59 60 return c.subscribeTCP(destination) 61 } 62 63 // SubscribeReplyDestination subscribe to a reply destination (this will create an internal subscription to the 64 // destination, but won't actually send the request over the wire to the broker. This is because when using temp 65 // queues, the destinations are dynamic. The raw socket is send responses that are to a destination that 66 // does not actually exist when using reply-to so this will allow that imaginary destination to operate. 67 func (c *connection) SubscribeReplyDestination(destination string) (Subscription, error) { 68 // check if the subscription exists, if so, return it. 69 if c == nil { 70 return nil, fmt.Errorf("cannot subscribe to '%s', no connection to broker", destination) 71 } 72 73 c.connLock.Lock() 74 if sub, ok := c.subscriptions[destination]; ok { 75 c.connLock.Unlock() 76 return sub, nil 77 } 78 c.connLock.Unlock() 79 80 // use websocket, if not use stomp TCP. 81 if c.useWs { 82 return c.subscribeWs(destination) 83 } 84 85 return c.subscribeTCPUsingReplyDestination(destination) 86 } 87 88 // Disconnect from broker, will close all channels 89 func (c *connection) Disconnect() (err error) { 90 if c == nil { 91 return fmt.Errorf("cannot disconnect, not connected") 92 } 93 if c.useWs { 94 if c.wsConn != nil && c.wsConn.connected { 95 defer c.cleanUpConnection() 96 err = c.wsConn.Disconnect() 97 } 98 } else { 99 if c.conn != nil { 100 defer c.cleanUpConnection() 101 err = c.conn.Disconnect() 102 } 103 } 104 return err 105 } 106 107 func (c *connection) cleanUpConnection() { 108 if c.conn != nil { 109 c.conn = nil 110 } 111 if c.wsConn != nil { 112 c.wsConn = nil 113 } 114 } 115 116 func (c *connection) subscribeWs(destination string) (Subscription, error) { 117 c.connLock.Lock() 118 defer c.connLock.Unlock() 119 if c.wsConn != nil { 120 wsSub := c.wsConn.Subscribe(destination) 121 sub := &subscription{wsStompSub: wsSub, id: wsSub.Id, c: wsSub.C, destination: destination} 122 c.subscriptions[destination] = sub 123 return sub, nil 124 } 125 return nil, fmt.Errorf("cannot subscribe, websocket not connected / established") 126 } 127 128 func (c *connection) subscribeTCP(destination string) (Subscription, error) { 129 c.connLock.Lock() 130 defer c.connLock.Unlock() 131 if c.conn != nil { 132 sub, _ := c.conn.Subscribe(destination, stomp.AckAuto) 133 id := uuid.New() 134 destChan := make(chan *model.Message) 135 go c.listenTCPFrames(sub.C, destChan) 136 bcSub := &subscription{stompTCPSub: sub, id: &id, c: destChan} 137 c.subscriptions[destination] = bcSub 138 return bcSub, nil 139 } 140 return nil, fmt.Errorf("no STOMP TCP connection established") 141 } 142 143 func (c *connection) subscribeTCPUsingReplyDestination(destination string) (Subscription, error) { 144 c.connLock.Lock() 145 defer c.connLock.Unlock() 146 if c.conn != nil { 147 148 var reply = func(f *frame.Frame) error { 149 f.Header.Add("reply-to", destination) 150 return nil 151 } 152 153 sub, _ := c.conn.Subscribe(destination, stomp.AckAuto, reply) 154 id := uuid.New() 155 destChan := make(chan *model.Message) 156 go c.listenTCPFrames(sub.C, destChan) 157 bcSub := &subscription{stompTCPSub: sub, id: &id, c: destChan} 158 c.subscriptions[destination] = bcSub 159 return bcSub, nil 160 } 161 return nil, fmt.Errorf("no STOMP TCP connection established") 162 } 163 164 func (c *connection) listenTCPFrames(src chan *stomp.Message, dst chan *model.Message) { 165 defer func() { 166 if r := recover(); r != nil { 167 log.Println("subscription is closed, message undeliverable to closed channel.") 168 } 169 }() 170 for { 171 f := <-src 172 var body []byte 173 var dest string 174 if f != nil && f.Body != nil { 175 body = f.Body 176 } 177 if f != nil && len(f.Destination) > 0 { 178 dest = f.Destination 179 } 180 if f != nil { 181 cf := &model.MessageConfig{Payload: body, Destination: dest} 182 183 // transfer over known non-standard, but important frame headers if they are set 184 if replyTo, ok := f.Header.Contains("reply-to"); ok { // used by rabbitmq for temp queues 185 cf.Headers = []model.MessageHeader{{Label: "reply-to", Value: replyTo}} 186 } 187 188 m := model.GenerateResponse(cf) 189 dst <- m 190 } 191 } 192 } 193 194 // SendJSONMessage sends a []byte payload carrying JSON data to a destination. 195 func (c *connection) SendJSONMessage(destination string, payload []byte, opts ...func(*frame.Frame) error) error { 196 return c.SendMessage(destination, "application/json", payload, opts...) 197 } 198 199 // SendMessageWithReplyDestination is the same as SendMessage, but adds in a reply-to header automatically. 200 // This is generally used in conjunction with SubscribeReplyDestination 201 func (c *connection) SendMessageWithReplyDestination(destination string, replyDestination, contentType string, payload []byte, opts ...func(*frame.Frame) error) error { 202 var headerReplyTo = func(f *frame.Frame) error { 203 f.Header.Add("reply-to", replyDestination) 204 return nil 205 } 206 opts = append(opts, headerReplyTo) 207 return c.SendMessage(destination, contentType, payload, opts...) 208 } 209 210 // SendMessage will send a []byte payload to a destination. 211 func (c *connection) SendMessage(destination string, contentType string, payload []byte, opts ...func(*frame.Frame) error) error { 212 c.connLock.Lock() 213 defer c.connLock.Unlock() 214 if c != nil && !c.useWs && c.conn != nil { 215 c.conn.Send(destination, contentType, payload, opts...) 216 return nil 217 } 218 if c != nil && c.useWs && c.wsConn != nil { 219 c.wsConn.Send(destination, contentType, payload, opts...) 220 return nil 221 } 222 return fmt.Errorf("cannot send message, no connection") 223 224 }