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  }