github.com/lusis/distribution@v2.0.1+incompatible/notifications/http.go (about)

     1  package notifications
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"sync"
     9  	"time"
    10  )
    11  
    12  // httpSink implements a single-flight, http notification endpoint. This is
    13  // very lightweight in that it only makes an attempt at an http request.
    14  // Reliability should be provided by the caller.
    15  type httpSink struct {
    16  	url string
    17  
    18  	mu        sync.Mutex
    19  	closed    bool
    20  	client    *http.Client
    21  	listeners []httpStatusListener
    22  
    23  	// TODO(stevvooe): Allow one to configure the media type accepted by this
    24  	// sink and choose the serialization based on that.
    25  }
    26  
    27  // newHTTPSink returns an unreliable, single-flight http sink. Wrap in other
    28  // sinks for increased reliability.
    29  func newHTTPSink(u string, timeout time.Duration, headers http.Header, listeners ...httpStatusListener) *httpSink {
    30  	return &httpSink{
    31  		url:       u,
    32  		listeners: listeners,
    33  		client: &http.Client{
    34  			Transport: &headerRoundTripper{
    35  				Transport: http.DefaultTransport.(*http.Transport),
    36  				headers:   headers,
    37  			},
    38  			Timeout: timeout,
    39  		},
    40  	}
    41  }
    42  
    43  // httpStatusListener is called on various outcomes of sending notifications.
    44  type httpStatusListener interface {
    45  	success(status int, events ...Event)
    46  	failure(status int, events ...Event)
    47  	err(err error, events ...Event)
    48  }
    49  
    50  // Accept makes an attempt to notify the endpoint, returning an error if it
    51  // fails. It is the caller's responsibility to retry on error. The events are
    52  // accepted or rejected as a group.
    53  func (hs *httpSink) Write(events ...Event) error {
    54  	hs.mu.Lock()
    55  	defer hs.mu.Unlock()
    56  	defer hs.client.Transport.(*headerRoundTripper).CloseIdleConnections()
    57  
    58  	if hs.closed {
    59  		return ErrSinkClosed
    60  	}
    61  
    62  	envelope := Envelope{
    63  		Events: events,
    64  	}
    65  
    66  	// TODO(stevvooe): It is not ideal to keep re-encoding the request body on
    67  	// retry but we are going to do it to keep the code simple. It is likely
    68  	// we could change the event struct to manage its own buffer.
    69  
    70  	p, err := json.MarshalIndent(envelope, "", "   ")
    71  	if err != nil {
    72  		for _, listener := range hs.listeners {
    73  			listener.err(err, events...)
    74  		}
    75  		return fmt.Errorf("%v: error marshaling event envelope: %v", hs, err)
    76  	}
    77  
    78  	body := bytes.NewReader(p)
    79  	resp, err := hs.client.Post(hs.url, EventsMediaType, body)
    80  	if err != nil {
    81  		for _, listener := range hs.listeners {
    82  			listener.err(err, events...)
    83  		}
    84  
    85  		return fmt.Errorf("%v: error posting: %v", hs, err)
    86  	}
    87  	defer resp.Body.Close()
    88  
    89  	// The notifier will treat any 2xx or 3xx response as accepted by the
    90  	// endpoint.
    91  	switch {
    92  	case resp.StatusCode >= 200 && resp.StatusCode < 400:
    93  		for _, listener := range hs.listeners {
    94  			listener.success(resp.StatusCode, events...)
    95  		}
    96  
    97  		// TODO(stevvooe): This is a little accepting: we may want to support
    98  		// unsupported media type responses with retries using the correct
    99  		// media type. There may also be cases that will never work.
   100  
   101  		return nil
   102  	default:
   103  		for _, listener := range hs.listeners {
   104  			listener.failure(resp.StatusCode, events...)
   105  		}
   106  		return fmt.Errorf("%v: response status %v unaccepted", hs, resp.Status)
   107  	}
   108  }
   109  
   110  // Close the endpoint
   111  func (hs *httpSink) Close() error {
   112  	hs.mu.Lock()
   113  	defer hs.mu.Unlock()
   114  
   115  	if hs.closed {
   116  		return fmt.Errorf("httpsink: already closed")
   117  	}
   118  
   119  	hs.closed = true
   120  	return nil
   121  }
   122  
   123  func (hs *httpSink) String() string {
   124  	return fmt.Sprintf("httpSink{%s}", hs.url)
   125  }
   126  
   127  type headerRoundTripper struct {
   128  	*http.Transport // must be transport to support CancelRequest
   129  	headers         http.Header
   130  }
   131  
   132  func (hrt *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   133  	var nreq http.Request
   134  	nreq = *req
   135  	nreq.Header = make(http.Header)
   136  
   137  	merge := func(headers http.Header) {
   138  		for k, v := range headers {
   139  			nreq.Header[k] = append(nreq.Header[k], v...)
   140  		}
   141  	}
   142  
   143  	merge(req.Header)
   144  	merge(hrt.headers)
   145  
   146  	return hrt.Transport.RoundTrip(&nreq)
   147  }