github.com/vmware/transport-go@v1.3.4/plank/services/stock-ticker-service.go (about) 1 // Copyright 2019-2021 VMware, Inc. 2 // SPDX-License-Identifier: BSD-2-Clause 3 4 package services 5 6 import ( 7 "context" 8 "encoding/json" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "sync" 13 "time" 14 15 "github.com/google/uuid" 16 "github.com/gorilla/mux" 17 "github.com/vmware/transport-go/bus" 18 "github.com/vmware/transport-go/model" 19 "github.com/vmware/transport-go/plank/utils" 20 "github.com/vmware/transport-go/service" 21 "github.com/vmware/transport-go/stompserver" 22 "golang.org/x/net/context/ctxhttp" 23 ) 24 25 const ( 26 StockTickerServiceChannel = "stock-ticker-service" 27 StockTickerAPI = "https://finnhub.io/api/v1/quote" 28 ) 29 30 // TickerSnapshotData is the data structure for this demo service 31 type TickerSnapshotData struct { 32 CurrentPrice float64 `json:"c"` 33 Change float64 `json:"d"` 34 PercentChange float64 `json:"dp"` 35 HighDayPrice float64 `json:"h"` 36 LowDayPrice float64 `json:"l"` 37 OpenPrice float64 `json:"o"` 38 PreviousClosePrice float64 `json:"pc"` 39 LastUpdated int64 `json:"t"` 40 } 41 42 // StockTickerService is a more complex real life example where its job is to subscribe clients 43 // to price updates for a stock symbol. the service accepts a JSON-formatted request from the client 44 // that must be formatted like this: {"symbol": "<TICKER_SYMBOL>"}. 45 // 46 // once the service receives the request, it will schedule a job to query the stock price API 47 // for the provided symbol, retrieve the data and pipe it back to the client every thirty seconds. 48 // upon the connected client leaving, the service will remove from its cache the timer. 49 type StockTickerService struct { 50 tickerListenersMap map[string]*time.Ticker 51 lock sync.RWMutex 52 } 53 54 // NewStockTickerService returns a new instance of StockTickerService 55 func NewStockTickerService() *StockTickerService { 56 return &StockTickerService{ 57 tickerListenersMap: make(map[string]*time.Ticker), 58 } 59 } 60 61 // HandleServiceRequest accepts incoming requests and schedules a job to fetch stock price from 62 // a third party API and return the results back to the user. 63 func (ps *StockTickerService) HandleServiceRequest(request *model.Request, core service.FabricServiceCore) { 64 switch request.Request { 65 case "ticker_price_lookup": 66 input := request.Payload.(map[string]string) 67 response, err := queryStockTickerAPI(input["symbol"]) 68 if err != nil { 69 core.SendErrorResponse(request, 400, err.Error()) 70 return 71 } 72 // send the response back to the client 73 core.SendResponse(request, response) 74 break 75 76 case "ticker_price_update_stream": 77 // parse the request and extract user input from key "symbol" 78 input := request.Payload.(map[string]interface{}) 79 symbol := input["symbol"].(string) 80 81 // get the price immediately for the first request 82 response, err := queryStockTickerAPI(symbol) 83 if err != nil { 84 core.SendErrorResponse(request, 400, err.Error()) 85 return 86 } 87 // send the response back to the client 88 core.SendResponse(request, response) 89 90 // set a ticker that fires every 30 seconds and keep it in a map for later disposal 91 ps.lock.Lock() 92 ticker := time.NewTicker(30 * time.Second) 93 ps.tickerListenersMap[request.BrokerDestination.ConnectionId] = ticker 94 ps.lock.Unlock() 95 96 // set up a handler for every time a ticker fires. 97 go func() { 98 for { 99 select { 100 case <-ticker.C: 101 response, err = queryStockTickerAPI(symbol) 102 if err != nil { 103 core.SendErrorResponse(request, 500, err.Error()) 104 continue 105 } 106 107 // log message to demonstrate that once the client disconnects 108 // the server disposes of the ticker to prevent memory leak. 109 utils.Log.Infoln("sending...") 110 111 // send the response back to the client 112 core.SendResponse(request, response) 113 } 114 } 115 }() 116 default: 117 core.HandleUnknownRequest(request) 118 } 119 } 120 121 // OnServiceReady sets up a listener to monitor the client STOMP sessions disconnecting from 122 // their sessions so that it can stop the running ticker and destroy it from the map structure 123 // for individual disconnected clients. this will prevent the service from making unnecessary 124 // HTTP calls for the clients even after they are gone and also the memory consumed from 125 // ever growing with each connection. 126 func (ps *StockTickerService) OnServiceReady() chan bool { 127 sessionNotifyHandler, _ := bus.GetBus().ListenStream(bus.STOMP_SESSION_NOTIFY_CHANNEL) 128 sessionNotifyHandler.Handle(func(message *model.Message) { 129 stompSessionEvt := message.Payload.(*bus.StompSessionEvent) 130 if stompSessionEvt.EventType == stompserver.ConnectionClosed || 131 stompSessionEvt.EventType == stompserver.UnsubscribeFromTopic { 132 if ticker, exists := ps.tickerListenersMap[stompSessionEvt.Id]; exists { 133 ticker.Stop() 134 ps.lock.Lock() 135 delete(ps.tickerListenersMap, stompSessionEvt.Id) 136 ps.lock.Unlock() 137 utils.Log.Warnf("timer cleaned for %s. trigger: %v", stompSessionEvt.Id, stompSessionEvt.EventType) 138 } 139 } 140 }, func(err error) {}) 141 readyChan := make(chan bool, 1) 142 readyChan <- true 143 return readyChan 144 } 145 146 // OnServerShutdown removes the running tickers 147 func (ps *StockTickerService) OnServerShutdown() { 148 ps.lock.Lock() 149 defer ps.lock.Unlock() 150 151 for _, ticker := range ps.tickerListenersMap { 152 ticker.Stop() 153 } 154 return 155 } 156 157 // GetRESTBridgeConfig returns a config for a REST endpoint that performs the same action as the STOMP variant 158 // except that there will be only one response instead of every 30 seconds. 159 func (ps *StockTickerService) GetRESTBridgeConfig() []*service.RESTBridgeConfig { 160 return []*service.RESTBridgeConfig{ 161 { 162 ServiceChannel: StockTickerServiceChannel, 163 Uri: "/rest/stock-ticker/{symbol}", 164 Method: http.MethodGet, 165 AllowHead: true, 166 AllowOptions: true, 167 FabricRequestBuilder: func(w http.ResponseWriter, r *http.Request) model.Request { 168 pathParams := mux.Vars(r) 169 return model.Request{ 170 Id: &uuid.UUID{}, 171 Payload: map[string]string{"symbol": pathParams["symbol"]}, 172 Request: "ticker_price_lookup", 173 BrokerDestination: nil, 174 } 175 }, 176 }, 177 } 178 } 179 180 // newTickerRequest is a convenient function that takes symbol as an input and returns 181 // a new HTTP request object along with any error 182 func newTickerRequest(symbol string) (*http.Request, error) { 183 uv := url.Values{} 184 uv.Set("symbol", symbol) 185 186 req, err := http.NewRequest("GET", StockTickerAPI, nil) 187 if err != nil { 188 return nil, err 189 } 190 req.URL.RawQuery = uv.Encode() 191 req.Header.Set("X-Finnhub-Token", "sandbox_c4l951aad3iftk6rfja0") 192 return req, nil 193 } 194 195 // queryStockTickerAPI performs an HTTP request against the Stock Ticker API and returns the results 196 // as a generic map[string]interface{} structure. if there's any error during the request-response cycle 197 // a nil will be returned followed by an error object. 198 func queryStockTickerAPI(symbol string) (map[string]interface{}, error) { 199 // craft a new HTTP request for the stock price provider API 200 req, err := newTickerRequest(symbol) 201 if err != nil { 202 return nil, err 203 } 204 205 // perform an HTTP call 206 rsp, err := ctxhttp.Do(context.Background(), http.DefaultClient, req) 207 if err != nil { 208 return nil, err 209 } 210 211 // parse the response from the HTTP call 212 defer rsp.Body.Close() 213 tickerData := &TickerSnapshotData{} 214 b, err := ioutil.ReadAll(rsp.Body) 215 if err != nil { 216 return nil, err 217 } 218 219 if err = json.Unmarshal(b, tickerData); err != nil { 220 return nil, err 221 } 222 223 return map[string]interface{}{ 224 "symbol": symbol, 225 "lastRefreshed": time.Unix(tickerData.LastUpdated, 0).String(), 226 "currentPrice": tickerData.CurrentPrice, 227 "change": tickerData.Change, 228 "highDayPrice": tickerData.HighDayPrice, 229 "lowDayPrice": tickerData.LowDayPrice, 230 "openPrice": tickerData.OpenPrice, 231 "closePrice": tickerData.PreviousClosePrice, // for backward compatible with UI examples 232 "previousClosePrice": tickerData.PreviousClosePrice, 233 }, nil 234 }