github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/proxy/proxyblobstore_test.go (about)

     1  package proxy
     2  
     3  import (
     4  	"io/ioutil"
     5  	"math/rand"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/docker/distribution"
    13  	"github.com/docker/distribution/context"
    14  	"github.com/docker/distribution/digest"
    15  	"github.com/docker/distribution/reference"
    16  	"github.com/docker/distribution/registry/proxy/scheduler"
    17  	"github.com/docker/distribution/registry/storage"
    18  	"github.com/docker/distribution/registry/storage/cache/memory"
    19  	"github.com/docker/distribution/registry/storage/driver/filesystem"
    20  	"github.com/docker/distribution/registry/storage/driver/inmemory"
    21  )
    22  
    23  var sbsMu sync.Mutex
    24  
    25  type statsBlobStore struct {
    26  	stats map[string]int
    27  	blobs distribution.BlobStore
    28  }
    29  
    30  func (sbs statsBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
    31  	sbsMu.Lock()
    32  	sbs.stats["put"]++
    33  	sbsMu.Unlock()
    34  
    35  	return sbs.blobs.Put(ctx, mediaType, p)
    36  }
    37  
    38  func (sbs statsBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
    39  	sbsMu.Lock()
    40  	sbs.stats["get"]++
    41  	sbsMu.Unlock()
    42  
    43  	return sbs.blobs.Get(ctx, dgst)
    44  }
    45  
    46  func (sbs statsBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
    47  	sbsMu.Lock()
    48  	sbs.stats["create"]++
    49  	sbsMu.Unlock()
    50  
    51  	return sbs.blobs.Create(ctx, options...)
    52  }
    53  
    54  func (sbs statsBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
    55  	sbsMu.Lock()
    56  	sbs.stats["resume"]++
    57  	sbsMu.Unlock()
    58  
    59  	return sbs.blobs.Resume(ctx, id)
    60  }
    61  
    62  func (sbs statsBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
    63  	sbsMu.Lock()
    64  	sbs.stats["open"]++
    65  	sbsMu.Unlock()
    66  
    67  	return sbs.blobs.Open(ctx, dgst)
    68  }
    69  
    70  func (sbs statsBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
    71  	sbsMu.Lock()
    72  	sbs.stats["serveblob"]++
    73  	sbsMu.Unlock()
    74  
    75  	return sbs.blobs.ServeBlob(ctx, w, r, dgst)
    76  }
    77  
    78  func (sbs statsBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
    79  
    80  	sbsMu.Lock()
    81  	sbs.stats["stat"]++
    82  	sbsMu.Unlock()
    83  
    84  	return sbs.blobs.Stat(ctx, dgst)
    85  }
    86  
    87  func (sbs statsBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
    88  	sbsMu.Lock()
    89  	sbs.stats["delete"]++
    90  	sbsMu.Unlock()
    91  
    92  	return sbs.blobs.Delete(ctx, dgst)
    93  }
    94  
    95  type testEnv struct {
    96  	numUnique int
    97  	inRemote  []distribution.Descriptor
    98  	store     proxyBlobStore
    99  	ctx       context.Context
   100  }
   101  
   102  func (te *testEnv) LocalStats() *map[string]int {
   103  	sbsMu.Lock()
   104  	ls := te.store.localStore.(statsBlobStore).stats
   105  	sbsMu.Unlock()
   106  	return &ls
   107  }
   108  
   109  func (te *testEnv) RemoteStats() *map[string]int {
   110  	sbsMu.Lock()
   111  	rs := te.store.remoteStore.(statsBlobStore).stats
   112  	sbsMu.Unlock()
   113  	return &rs
   114  }
   115  
   116  // Populate remote store and record the digests
   117  func makeTestEnv(t *testing.T, name string) *testEnv {
   118  	nameRef, err := reference.ParseNamed(name)
   119  	if err != nil {
   120  		t.Fatalf("unable to parse reference: %s", err)
   121  	}
   122  
   123  	ctx := context.Background()
   124  
   125  	truthDir, err := ioutil.TempDir("", "truth")
   126  	if err != nil {
   127  		t.Fatalf("unable to create tempdir: %s", err)
   128  	}
   129  
   130  	cacheDir, err := ioutil.TempDir("", "cache")
   131  	if err != nil {
   132  		t.Fatalf("unable to create tempdir: %s", err)
   133  	}
   134  
   135  	// todo: create a tempfile area here
   136  	localRegistry, err := storage.NewRegistry(ctx, filesystem.New(truthDir), storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableRedirect, storage.DisableDigestResumption)
   137  	if err != nil {
   138  		t.Fatalf("error creating registry: %v", err)
   139  	}
   140  	localRepo, err := localRegistry.Repository(ctx, nameRef)
   141  	if err != nil {
   142  		t.Fatalf("unexpected error getting repo: %v", err)
   143  	}
   144  
   145  	truthRegistry, err := storage.NewRegistry(ctx, filesystem.New(cacheDir), storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()))
   146  	if err != nil {
   147  		t.Fatalf("error creating registry: %v", err)
   148  	}
   149  	truthRepo, err := truthRegistry.Repository(ctx, nameRef)
   150  	if err != nil {
   151  		t.Fatalf("unexpected error getting repo: %v", err)
   152  	}
   153  
   154  	truthBlobs := statsBlobStore{
   155  		stats: make(map[string]int),
   156  		blobs: truthRepo.Blobs(ctx),
   157  	}
   158  
   159  	localBlobs := statsBlobStore{
   160  		stats: make(map[string]int),
   161  		blobs: localRepo.Blobs(ctx),
   162  	}
   163  
   164  	s := scheduler.New(ctx, inmemory.New(), "/scheduler-state.json")
   165  
   166  	proxyBlobStore := proxyBlobStore{
   167  		repositoryName: nameRef,
   168  		remoteStore:    truthBlobs,
   169  		localStore:     localBlobs,
   170  		scheduler:      s,
   171  		authChallenger: &mockChallenger{},
   172  	}
   173  
   174  	te := &testEnv{
   175  		store: proxyBlobStore,
   176  		ctx:   ctx,
   177  	}
   178  	return te
   179  }
   180  
   181  func makeBlob(size int) []byte {
   182  	blob := make([]byte, size, size)
   183  	for i := 0; i < size; i++ {
   184  		blob[i] = byte('A' + rand.Int()%48)
   185  	}
   186  	return blob
   187  }
   188  
   189  func init() {
   190  	rand.Seed(42)
   191  }
   192  
   193  func perm(m []distribution.Descriptor) []distribution.Descriptor {
   194  	for i := 0; i < len(m); i++ {
   195  		j := rand.Intn(i + 1)
   196  		tmp := m[i]
   197  		m[i] = m[j]
   198  		m[j] = tmp
   199  	}
   200  	return m
   201  }
   202  
   203  func populate(t *testing.T, te *testEnv, blobCount, size, numUnique int) {
   204  	var inRemote []distribution.Descriptor
   205  
   206  	for i := 0; i < numUnique; i++ {
   207  		bytes := makeBlob(size)
   208  		for j := 0; j < blobCount/numUnique; j++ {
   209  			desc, err := te.store.remoteStore.Put(te.ctx, "", bytes)
   210  			if err != nil {
   211  				t.Fatalf("Put in store")
   212  			}
   213  
   214  			inRemote = append(inRemote, desc)
   215  		}
   216  	}
   217  
   218  	te.inRemote = inRemote
   219  	te.numUnique = numUnique
   220  }
   221  func TestProxyStoreGet(t *testing.T) {
   222  	te := makeTestEnv(t, "foo/bar")
   223  
   224  	localStats := te.LocalStats()
   225  	remoteStats := te.RemoteStats()
   226  
   227  	populate(t, te, 1, 10, 1)
   228  	_, err := te.store.Get(te.ctx, te.inRemote[0].Digest)
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  
   233  	if (*localStats)["get"] != 1 && (*localStats)["put"] != 1 {
   234  		t.Errorf("Unexpected local counts")
   235  	}
   236  
   237  	if (*remoteStats)["get"] != 1 {
   238  		t.Errorf("Unexpected remote get count")
   239  	}
   240  
   241  	_, err = te.store.Get(te.ctx, te.inRemote[0].Digest)
   242  	if err != nil {
   243  		t.Fatal(err)
   244  	}
   245  
   246  	if (*localStats)["get"] != 2 && (*localStats)["put"] != 1 {
   247  		t.Errorf("Unexpected local counts")
   248  	}
   249  
   250  	if (*remoteStats)["get"] != 1 {
   251  		t.Errorf("Unexpected remote get count")
   252  	}
   253  
   254  }
   255  
   256  func TestProxyStoreStat(t *testing.T) {
   257  	te := makeTestEnv(t, "foo/bar")
   258  
   259  	remoteBlobCount := 1
   260  	populate(t, te, remoteBlobCount, 10, 1)
   261  
   262  	localStats := te.LocalStats()
   263  	remoteStats := te.RemoteStats()
   264  
   265  	// Stat - touches both stores
   266  	for _, d := range te.inRemote {
   267  		_, err := te.store.Stat(te.ctx, d.Digest)
   268  		if err != nil {
   269  			t.Fatalf("Error stating proxy store")
   270  		}
   271  	}
   272  
   273  	if (*localStats)["stat"] != remoteBlobCount {
   274  		t.Errorf("Unexpected local stat count")
   275  	}
   276  
   277  	if (*remoteStats)["stat"] != remoteBlobCount {
   278  		t.Errorf("Unexpected remote stat count")
   279  	}
   280  
   281  	if te.store.authChallenger.(*mockChallenger).count != len(te.inRemote) {
   282  		t.Fatalf("Unexpected auth challenge count, got %#v", te.store.authChallenger)
   283  	}
   284  
   285  }
   286  
   287  func TestProxyStoreServeHighConcurrency(t *testing.T) {
   288  	te := makeTestEnv(t, "foo/bar")
   289  	blobSize := 200
   290  	blobCount := 10
   291  	numUnique := 1
   292  	populate(t, te, blobCount, blobSize, numUnique)
   293  
   294  	numClients := 16
   295  	testProxyStoreServe(t, te, numClients)
   296  }
   297  
   298  func TestProxyStoreServeMany(t *testing.T) {
   299  	te := makeTestEnv(t, "foo/bar")
   300  	blobSize := 200
   301  	blobCount := 10
   302  	numUnique := 4
   303  	populate(t, te, blobCount, blobSize, numUnique)
   304  
   305  	numClients := 4
   306  	testProxyStoreServe(t, te, numClients)
   307  }
   308  
   309  // todo(richardscothern): blobCount must be smaller than num clients
   310  func TestProxyStoreServeBig(t *testing.T) {
   311  	te := makeTestEnv(t, "foo/bar")
   312  
   313  	blobSize := 2 << 20
   314  	blobCount := 4
   315  	numUnique := 2
   316  	populate(t, te, blobCount, blobSize, numUnique)
   317  
   318  	numClients := 4
   319  	testProxyStoreServe(t, te, numClients)
   320  }
   321  
   322  // testProxyStoreServe will create clients to consume all blobs
   323  // populated in the truth store
   324  func testProxyStoreServe(t *testing.T, te *testEnv, numClients int) {
   325  	localStats := te.LocalStats()
   326  	remoteStats := te.RemoteStats()
   327  
   328  	var wg sync.WaitGroup
   329  
   330  	for i := 0; i < numClients; i++ {
   331  		// Serveblob - pulls through blobs
   332  		wg.Add(1)
   333  		go func() {
   334  			defer wg.Done()
   335  			for _, remoteBlob := range te.inRemote {
   336  				w := httptest.NewRecorder()
   337  				r, err := http.NewRequest("GET", "", nil)
   338  				if err != nil {
   339  					t.Fatal(err)
   340  				}
   341  
   342  				err = te.store.ServeBlob(te.ctx, w, r, remoteBlob.Digest)
   343  				if err != nil {
   344  					t.Fatalf(err.Error())
   345  				}
   346  
   347  				bodyBytes := w.Body.Bytes()
   348  				localDigest := digest.FromBytes(bodyBytes)
   349  				if localDigest != remoteBlob.Digest {
   350  					t.Fatalf("Mismatching blob fetch from proxy")
   351  				}
   352  			}
   353  		}()
   354  	}
   355  
   356  	wg.Wait()
   357  
   358  	remoteBlobCount := len(te.inRemote)
   359  	if (*localStats)["stat"] != remoteBlobCount*numClients && (*localStats)["create"] != te.numUnique {
   360  		t.Fatal("Expected: stat:", remoteBlobCount*numClients, "create:", remoteBlobCount)
   361  	}
   362  
   363  	// Wait for any async storage goroutines to finish
   364  	time.Sleep(3 * time.Second)
   365  
   366  	remoteStatCount := (*remoteStats)["stat"]
   367  	remoteOpenCount := (*remoteStats)["open"]
   368  
   369  	// Serveblob - blobs come from local
   370  	for _, dr := range te.inRemote {
   371  		w := httptest.NewRecorder()
   372  		r, err := http.NewRequest("GET", "", nil)
   373  		if err != nil {
   374  			t.Fatal(err)
   375  		}
   376  
   377  		err = te.store.ServeBlob(te.ctx, w, r, dr.Digest)
   378  		if err != nil {
   379  			t.Fatalf(err.Error())
   380  		}
   381  
   382  		dl := digest.FromBytes(w.Body.Bytes())
   383  		if dl != dr.Digest {
   384  			t.Errorf("Mismatching blob fetch from proxy")
   385  		}
   386  	}
   387  
   388  	localStats = te.LocalStats()
   389  	remoteStats = te.RemoteStats()
   390  
   391  	// Ensure remote unchanged
   392  	if (*remoteStats)["stat"] != remoteStatCount && (*remoteStats)["open"] != remoteOpenCount {
   393  		t.Fatalf("unexpected remote stats: %#v", remoteStats)
   394  	}
   395  }