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  }