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 }