github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/http/longpoll/client/client.go (about) 1 package client 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "log" 8 "net/http" 9 "net/url" 10 "sync/atomic" 11 "time" 12 ) 13 14 const ( 15 DEFAULT_TIMEOUT = 30 16 DEFAULT_REATTEMPT = 30 17 ) 18 19 type PollResponse struct { 20 Events []PollEvent `json:"events"` 21 Timestamp int64 `json:"timestamp"` 22 } 23 24 // Taken from https://github.com/jcuga/golongpoll/blob/master/events.go 25 type PollEvent struct { 26 // Timestamp is milliseconds since epoch to match javascrits Date.getTime() 27 Timestamp int64 `json:"timestamp"` 28 Category string `json:"category"` 29 // NOTE: Data can be anything that is able to passed to json.Marshal() 30 Data json.RawMessage `json:"data"` 31 } 32 33 type Client struct { 34 url *url.URL 35 category string 36 // Timeout controls the timeout in all the requests, can be changed after instantiating the client 37 Timeout uint64 38 // Reattempt controls the amount of time the client waits to reconnect to the server after a failure 39 Reattempt time.Duration 40 // Will get all the events data 41 EventsChan chan PollEvent 42 // Flag that tracks the current run ID 43 runID uint64 44 // The HTTP client to perform the requests, any changes on this should be done prior to starting the client the first time 45 HttpClient *http.Client 46 // The username to be used for basic HTTP authentication 47 BasicAuthUsername string 48 // The password to be used for basic HTTP authentication 49 BasicAuthPassword string 50 51 // Whether or not logging should be enabled 52 LoggingEnabled bool 53 } 54 55 // Instantiate a new client to connect to a given URL and send the events into a channel 56 // The URL shouldn't contain any GET parameters although its fine if it contains some but category, since_time and timeout will be overriten 57 // stubChanData must either be an empty structure of the events data or a map[string]interface{} if the events do not follow a specific structure 58 func NewClient(url *url.URL, category string) *Client { 59 return &Client{ 60 url: url, 61 category: category, 62 Timeout: DEFAULT_TIMEOUT, 63 Reattempt: DEFAULT_REATTEMPT * time.Second, 64 EventsChan: make(chan PollEvent), 65 HttpClient: &http.Client{}, 66 LoggingEnabled: true, 67 } 68 } 69 70 // Start the polling of the events on the URL defined in the client 71 // Will send the events in the EventsChan of the client 72 func (c *Client) Start() { 73 u := c.url 74 if c.LoggingEnabled { 75 log.Println("Now observing changes on", u.String()) 76 } 77 78 atomic.AddUint64(&(c.runID), 1) 79 currentRunID := atomic.LoadUint64(&(c.runID)) 80 81 go func(runID uint64, u *url.URL) { 82 since := time.Now().Unix() * 1000 83 for { 84 pr, err := c.fetchEvents(since) 85 86 if err != nil { 87 if c.LoggingEnabled { 88 log.Println(err) 89 log.Printf("Reattempting to connect to %s in %d seconds", u.String(), c.Reattempt) 90 } 91 time.Sleep(c.Reattempt) 92 continue 93 } 94 95 // We check that its still the same runID as when this goroutine was started 96 clientRunID := atomic.LoadUint64(&(c.runID)) 97 if clientRunID != runID { 98 if c.LoggingEnabled { 99 log.Printf("Client on URL %s has been stopped, not sending events", u.String()) 100 } 101 return 102 } 103 104 if len(pr.Events) > 0 { 105 if c.LoggingEnabled { 106 log.Println("Got", len(pr.Events), "event(s) from URL", u.String()) 107 } 108 for _, event := range pr.Events { 109 since = event.Timestamp 110 c.EventsChan <- event 111 } 112 } else { 113 // Only push timestamp forward if its greater than the last we checked 114 if pr.Timestamp > since { 115 since = pr.Timestamp 116 } 117 } 118 } 119 }(currentRunID, u) 120 } 121 122 func (c *Client) Stop() { 123 // Changing the runID will have any previous goroutine ignore any events it may receive 124 atomic.AddUint64(&(c.runID), 1) 125 } 126 127 // Call the longpoll server to get the events since a specific timestamp 128 func (c Client) fetchEvents(since int64) (PollResponse, error) { 129 u := c.url 130 if c.LoggingEnabled { 131 log.Println("Checking for changes events since", since, "on URL", u.String()) 132 } 133 134 query := u.Query() 135 query.Set("category", c.category) 136 query.Set("since_time", fmt.Sprintf("%d", since)) 137 query.Set("timeout", fmt.Sprintf("%d", c.Timeout)) 138 u.RawQuery = query.Encode() 139 140 req, _ := http.NewRequest("GET", u.String(), nil) 141 if c.BasicAuthUsername != "" && c.BasicAuthPassword != "" { 142 req.SetBasicAuth(c.BasicAuthUsername, c.BasicAuthPassword) 143 } 144 145 resp, err := c.HttpClient.Do(req) 146 if err != nil { 147 msg := fmt.Sprintf("Error while connecting to %s to observe changes. Error was: %s", u, err) 148 return PollResponse{}, errors.New(msg) 149 } 150 151 if resp.StatusCode != http.StatusOK { 152 msg := fmt.Sprintf("Wrong status code received from longpoll server: %d", resp.StatusCode) 153 return PollResponse{}, errors.New(msg) 154 } 155 156 decoder := json.NewDecoder(resp.Body) 157 defer resp.Body.Close() 158 159 var pr PollResponse 160 err = decoder.Decode(&pr) 161 if err != nil { 162 if c.LoggingEnabled { 163 log.Println("Error while decoding poll response: %s", err) 164 } 165 return PollResponse{}, err 166 } 167 168 return pr, nil 169 }