github.com/uber/kraken@v0.1.4/lib/store/ca_store.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 store
    15  
    16  import (
    17  	"fmt"
    18  	"hash"
    19  	"io"
    20  	"os"
    21  	"path"
    22  
    23  	"github.com/uber/kraken/core"
    24  	"github.com/uber/kraken/lib/hrw"
    25  	"github.com/uber/kraken/lib/store/base"
    26  	"github.com/andres-erbsen/clock"
    27  	"github.com/docker/distribution/uuid"
    28  	"github.com/spaolacci/murmur3"
    29  	"github.com/uber-go/tally"
    30  )
    31  
    32  // CAStore allows uploading / caching content-addressable files.
    33  type CAStore struct {
    34  	*uploadStore
    35  	*cacheStore
    36  	cleanup *cleanupManager
    37  }
    38  
    39  // NewCAStore creates a new CAStore.
    40  func NewCAStore(config CAStoreConfig, stats tally.Scope) (*CAStore, error) {
    41  	config = config.applyDefaults()
    42  
    43  	stats = stats.Tagged(map[string]string{
    44  		"module": "castore",
    45  	})
    46  
    47  	uploadStore, err := newUploadStore(config.UploadDir)
    48  	if err != nil {
    49  		return nil, fmt.Errorf("new upload store: %s", err)
    50  	}
    51  
    52  	cacheBackend := base.NewCASFileStoreWithLRUMap(config.Capacity, clock.New())
    53  	cacheStore, err := newCacheStore(config.CacheDir, cacheBackend)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("new cache store: %s", err)
    56  	}
    57  
    58  	if err := initCASVolumes(config.CacheDir, config.Volumes); err != nil {
    59  		return nil, fmt.Errorf("init cas volumes: %s", err)
    60  	}
    61  
    62  	cleanup, err := newCleanupManager(clock.New(), stats)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("new cleanup manager: %s", err)
    65  	}
    66  	cleanup.addJob("upload", config.UploadCleanup, uploadStore.newFileOp())
    67  	cleanup.addJob("cache", config.CacheCleanup, cacheStore.newFileOp())
    68  
    69  	return &CAStore{uploadStore, cacheStore, cleanup}, nil
    70  }
    71  
    72  // Close terminates any goroutines started by s.
    73  func (s *CAStore) Close() {
    74  	s.cleanup.stop()
    75  }
    76  
    77  // MoveUploadFileToCache commits uploadName as cacheName. Clients are expected
    78  // to validate the content of the upload file matches the cacheName digest.
    79  func (s *CAStore) MoveUploadFileToCache(uploadName, cacheName string) error {
    80  	uploadPath, err := s.uploadStore.newFileOp().GetFilePath(uploadName)
    81  	if err != nil {
    82  		return err
    83  	}
    84  	defer s.DeleteUploadFile(uploadName)
    85  	return s.cacheStore.newFileOp().MoveFileFrom(cacheName, s.cacheStore.state, uploadPath)
    86  }
    87  
    88  // CreateCacheFile initializes a cache file for name from r. name should be a raw
    89  // hex sha256 digest, and the contents of r must hash to name.
    90  func (s *CAStore) CreateCacheFile(name string, r io.Reader) error {
    91  	return s.WriteCacheFile(name, func(w FileReadWriter) error {
    92  		_, err := io.Copy(w, r)
    93  		return err
    94  	})
    95  }
    96  
    97  // WriteCacheFile initializes a cache file for name by passing a temporary
    98  // upload file writer to the write function.
    99  func (s *CAStore) WriteCacheFile(name string, write func(w FileReadWriter) error) error {
   100  	tmp := fmt.Sprintf("%s.%s", name, uuid.Generate().String())
   101  	if err := s.CreateUploadFile(tmp, 0); err != nil {
   102  		return fmt.Errorf("create upload file: %s", err)
   103  	}
   104  	defer s.DeleteUploadFile(tmp)
   105  
   106  	w, err := s.GetUploadFileReadWriter(tmp)
   107  	if err != nil {
   108  		return fmt.Errorf("get upload writer: %s", err)
   109  	}
   110  	defer w.Close()
   111  
   112  	if err := write(w); err != nil {
   113  		return err
   114  	}
   115  
   116  	if _, err := w.Seek(0, 0); err != nil {
   117  		return fmt.Errorf("seek: %s", err)
   118  	}
   119  	actual, err := core.NewDigester().FromReader(w)
   120  	if err != nil {
   121  		return fmt.Errorf("compute digest: %s", err)
   122  	}
   123  	expected, err := core.NewSHA256DigestFromHex(name)
   124  	if err != nil {
   125  		return fmt.Errorf("new digest from file name: %s", err)
   126  	}
   127  	if actual != expected {
   128  		return fmt.Errorf("failed to verify data: digests do not match")
   129  	}
   130  
   131  	if err := s.MoveUploadFileToCache(tmp, name); err != nil && !os.IsExist(err) {
   132  		return fmt.Errorf("move upload file to cache: %s", err)
   133  	}
   134  	return nil
   135  }
   136  
   137  func initCASVolumes(dir string, volumes []Volume) error {
   138  	if len(volumes) == 0 {
   139  		return nil
   140  	}
   141  
   142  	rendezvousHash := hrw.NewRendezvousHash(
   143  		func() hash.Hash { return murmur3.New64() },
   144  		hrw.UInt64ToFloat64)
   145  
   146  	for _, v := range volumes {
   147  		if _, err := os.Stat(v.Location); err != nil {
   148  			return fmt.Errorf("verify volume: %s", err)
   149  		}
   150  		rendezvousHash.AddNode(v.Location, v.Weight)
   151  	}
   152  
   153  	// Create 256 symlinks under dir.
   154  	for subdirIndex := 0; subdirIndex < 256; subdirIndex++ {
   155  		subdirName := fmt.Sprintf("%02X", subdirIndex)
   156  		nodes := rendezvousHash.GetOrderedNodes(subdirName, 1)
   157  		if len(nodes) != 1 {
   158  			return fmt.Errorf("calculate volume for subdir: %s", subdirName)
   159  		}
   160  		sourcePath := path.Join(nodes[0].Label, path.Base(dir), subdirName)
   161  		if err := os.MkdirAll(sourcePath, 0775); err != nil {
   162  			return fmt.Errorf("volume source path: %s", err)
   163  		}
   164  		targetPath := path.Join(dir, subdirName)
   165  		if err := createOrUpdateSymlink(sourcePath, targetPath); err != nil {
   166  			return fmt.Errorf("symlink to volume: %s", err)
   167  		}
   168  	}
   169  
   170  	return nil
   171  }