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  }