github.com/uber/kraken@v0.1.4/lib/backend/hdfsbackend/webhdfs/client.go (about) 1 // Copyright (c) 2016-2019 Uber Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package webhdfs 15 16 import ( 17 "bytes" 18 "encoding/json" 19 "errors" 20 "fmt" 21 "io" 22 "net/http" 23 "net/url" 24 "path" 25 "strconv" 26 "time" 27 28 "github.com/cenkalti/backoff" 29 "github.com/uber/kraken/lib/backend/backenderrors" 30 "github.com/uber/kraken/utils/httputil" 31 "github.com/uber/kraken/utils/memsize" 32 ) 33 34 // Client wraps webhdfs operations. All paths must be absolute. 35 type Client interface { 36 Create(path string, src io.Reader) error 37 Rename(from, to string) error 38 Mkdirs(path string) error 39 Open(path string, dst io.Writer) error 40 GetFileStatus(path string) (FileStatus, error) 41 ListFileStatus(path string) ([]FileStatus, error) 42 } 43 44 type allNameNodesFailedError struct { 45 err error 46 } 47 48 func (e allNameNodesFailedError) Error() string { 49 return fmt.Sprintf("all name nodes failed: %s", e.err) 50 } 51 52 func retryable(err error) bool { 53 return httputil.IsForbidden(err) || httputil.IsNetworkError(err) 54 } 55 56 type client struct { 57 config Config 58 namenodes []string 59 username string 60 } 61 62 // NewClient creates a new Client. 63 func NewClient(config Config, namenodes []string, username string) (Client, error) { 64 config.applyDefaults() 65 if len(namenodes) == 0 { 66 return nil, errors.New("namenodes required") 67 } 68 return &client{config, namenodes, username}, nil 69 } 70 71 // nameNodeBackOff returns the backoff used on all http requests to namenodes. 72 // We use a fairly aggressive backoff to handle failover. 73 // 74 // TODO(codyg): Normally it is be the responsibility of the farthest downstream 75 // client to handle retry, however the Mesos fetcher does not currently support 76 // retry, and thus namenode failovers have caused task launch failures. Ideally 77 // we can remove this once the Mesos fetcher is more reliable. 78 func (c *client) nameNodeBackOff() backoff.BackOff { 79 b := &backoff.ExponentialBackOff{ 80 InitialInterval: 2 * time.Second, 81 RandomizationFactor: 0.05, 82 Multiplier: 2, 83 MaxInterval: 30 * time.Second, 84 Clock: backoff.SystemClock, 85 } 86 return backoff.WithMaxRetries(b, 5) 87 } 88 89 type exceededCapError error 90 91 // capBuffer is a buffer that returns errors if the buffer exceeds cap. 92 type capBuffer struct { 93 cap int64 94 buf *bytes.Buffer 95 } 96 97 func (b *capBuffer) Write(p []byte) (n int, err error) { 98 if int64(len(p)+b.buf.Len()) > b.cap { 99 return 0, exceededCapError( 100 fmt.Errorf("buffer exceeded max capacity %s", memsize.Format(uint64(b.cap)))) 101 } 102 return b.buf.Write(p) 103 } 104 105 type drainSrcError struct { 106 err error 107 } 108 109 func (e drainSrcError) Error() string { return fmt.Sprintf("drain src: %s", e.err) } 110 111 func (c *client) Create(path string, src io.Reader) error { 112 // We must be able to replay src in the event that uploading to the data node 113 // fails halfway through the upload, thus we attempt to upcast src to an io.Seeker 114 // for this purpose. If src is not an io.Seeker, we drain it to an in-memory buffer 115 // that can be replayed. 116 readSeeker, ok := src.(io.ReadSeeker) 117 if !ok { 118 var b []byte 119 if buf, ok := src.(*bytes.Buffer); ok { 120 // Optimization to avoid draining an existing buffer. 121 b = buf.Bytes() 122 } else { 123 cbuf := &capBuffer{int64(c.config.BufferGuard), new(bytes.Buffer)} 124 if _, err := io.Copy(cbuf, src); err != nil { 125 return drainSrcError{err} 126 } 127 b = cbuf.buf.Bytes() 128 } 129 readSeeker = bytes.NewReader(b) 130 } 131 132 v := c.values() 133 v.Set("op", "CREATE") 134 v.Set("buffersize", strconv.FormatInt(int64(c.config.BufferSize), 10)) 135 v.Set("overwrite", "true") 136 137 var nameresp, dataresp *http.Response 138 var nnErr error 139 for _, nn := range c.namenodes { 140 nameresp, nnErr = httputil.Put( 141 getURL(nn, path, v), 142 httputil.SendRetry(httputil.RetryBackoff(c.nameNodeBackOff())), 143 httputil.SendRedirect(func(req *http.Request, via []*http.Request) error { 144 return http.ErrUseLastResponse 145 }), 146 httputil.SendAcceptedCodes(http.StatusTemporaryRedirect, http.StatusPermanentRedirect)) 147 if nnErr != nil { 148 if retryable(nnErr) { 149 continue 150 } 151 return nnErr 152 } 153 defer nameresp.Body.Close() 154 155 // Follow redirect location manually per WebHDFS protocol. 156 loc, ok := nameresp.Header["Location"] 157 if !ok || len(loc) == 0 { 158 return fmt.Errorf("missing location field in response header: %s", nameresp.Header) 159 } 160 161 dataresp, nnErr = httputil.Put( 162 loc[0], 163 httputil.SendBody(readSeeker), 164 httputil.SendAcceptedCodes(http.StatusCreated)) 165 if nnErr != nil { 166 if retryable(nnErr) { 167 // Reset reader for next retry. 168 if _, err := readSeeker.Seek(0, io.SeekStart); err != nil { 169 return fmt.Errorf("seek: %s", err) 170 } 171 continue 172 } 173 return nnErr 174 } 175 defer dataresp.Body.Close() 176 177 return nil 178 } 179 return allNameNodesFailedError{nnErr} 180 } 181 182 func (c *client) Rename(from, to string) error { 183 v := c.values() 184 v.Set("op", "RENAME") 185 v.Set("destination", to) 186 187 var resp *http.Response 188 var nnErr error 189 for _, nn := range c.namenodes { 190 resp, nnErr = httputil.Put( 191 getURL(nn, from, v), 192 httputil.SendRetry(httputil.RetryBackoff(c.nameNodeBackOff()))) 193 if nnErr != nil { 194 if retryable(nnErr) { 195 continue 196 } 197 return nnErr 198 } 199 resp.Body.Close() 200 return nil 201 } 202 return allNameNodesFailedError{nnErr} 203 } 204 205 func (c *client) Mkdirs(path string) error { 206 v := c.values() 207 v.Set("op", "MKDIRS") 208 v.Set("permission", "777") 209 210 var resp *http.Response 211 var nnErr error 212 for _, nn := range c.namenodes { 213 resp, nnErr = httputil.Put( 214 getURL(nn, path, v), 215 httputil.SendRetry(httputil.RetryBackoff(c.nameNodeBackOff()))) 216 if nnErr != nil { 217 if retryable(nnErr) { 218 continue 219 } 220 return nnErr 221 } 222 resp.Body.Close() 223 return nil 224 } 225 return allNameNodesFailedError{nnErr} 226 } 227 228 func (c *client) Open(path string, dst io.Writer) error { 229 v := c.values() 230 v.Set("op", "OPEN") 231 v.Set("buffersize", strconv.FormatInt(int64(c.config.BufferSize), 10)) 232 233 var resp *http.Response 234 var nnErr error 235 for _, nn := range c.namenodes { 236 // We retry 400s here because experience has shown the datanode this 237 // request gets redirected to is sometimes invalid, and will return a 400 238 // error. By retrying the request, we hope to eventually get redirected 239 // to a valid datanode. 240 resp, nnErr = httputil.Get( 241 getURL(nn, path, v), 242 httputil.SendRetry( 243 httputil.RetryBackoff(c.nameNodeBackOff()), 244 httputil.RetryCodes(http.StatusBadRequest))) 245 if nnErr != nil { 246 if retryable(nnErr) { 247 continue 248 } 249 if httputil.IsNotFound(nnErr) { 250 return backenderrors.ErrBlobNotFound 251 } 252 return nnErr 253 } 254 defer resp.Body.Close() 255 if n, err := io.Copy(dst, resp.Body); err != nil { 256 return fmt.Errorf("copy response: %s", err) 257 } else if n != resp.ContentLength { 258 return fmt.Errorf( 259 "transferred bytes %d does not match content length %d", n, resp.ContentLength) 260 } 261 return nil 262 } 263 return allNameNodesFailedError{nnErr} 264 } 265 266 func (c *client) GetFileStatus(path string) (FileStatus, error) { 267 v := c.values() 268 v.Set("op", "GETFILESTATUS") 269 270 var resp *http.Response 271 var nnErr error 272 for _, nn := range c.namenodes { 273 resp, nnErr = httputil.Get( 274 getURL(nn, path, v), 275 httputil.SendRetry(httputil.RetryBackoff(c.nameNodeBackOff()))) 276 if nnErr != nil { 277 if retryable(nnErr) { 278 continue 279 } 280 if httputil.IsNotFound(nnErr) { 281 return FileStatus{}, backenderrors.ErrBlobNotFound 282 } 283 return FileStatus{}, nnErr 284 } 285 defer resp.Body.Close() 286 var fsr fileStatusResponse 287 if err := json.NewDecoder(resp.Body).Decode(&fsr); err != nil { 288 return FileStatus{}, fmt.Errorf("decode body: %s", err) 289 } 290 return fsr.FileStatus, nil 291 } 292 return FileStatus{}, allNameNodesFailedError{nnErr} 293 } 294 295 func (c *client) ListFileStatus(path string) ([]FileStatus, error) { 296 v := c.values() 297 v.Set("op", "LISTSTATUS") 298 299 var resp *http.Response 300 var nnErr error 301 for _, nn := range c.namenodes { 302 resp, nnErr = httputil.Get( 303 getURL(nn, path, v), 304 httputil.SendRetry(httputil.RetryBackoff(c.nameNodeBackOff()))) 305 if nnErr != nil { 306 if retryable(nnErr) { 307 continue 308 } 309 return nil, nnErr 310 } 311 defer resp.Body.Close() 312 var lsr listStatusResponse 313 if err := json.NewDecoder(resp.Body).Decode(&lsr); err != nil { 314 return nil, fmt.Errorf("decode body: %s", err) 315 } 316 return lsr.FileStatuses.FileStatus, nil 317 } 318 return nil, allNameNodesFailedError{nnErr} 319 } 320 321 func (c *client) values() url.Values { 322 v := url.Values{} 323 if c.username != "" { 324 v.Set("user.name", c.username) 325 } 326 return v 327 } 328 329 func getURL(namenode, p string, v url.Values) string { 330 endpoint := path.Join("/webhdfs/v1", p) 331 return fmt.Sprintf("http://%s%s?%s", namenode, endpoint, v.Encode()) 332 }