2015-07-29 18:12:01 +00:00
|
|
|
package proxy
|
|
|
|
|
|
|
|
import (
|
2015-09-18 23:11:35 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"math/rand"
|
2015-07-29 18:12:01 +00:00
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2015-09-18 23:11:35 +00:00
|
|
|
"sync"
|
2015-07-29 18:12:01 +00:00
|
|
|
"testing"
|
2015-09-18 23:11:35 +00:00
|
|
|
"time"
|
2015-07-29 18:12:01 +00:00
|
|
|
|
|
|
|
"github.com/docker/distribution"
|
|
|
|
"github.com/docker/distribution/context"
|
|
|
|
"github.com/docker/distribution/digest"
|
2015-12-15 22:35:23 +00:00
|
|
|
"github.com/docker/distribution/reference"
|
2015-07-29 18:12:01 +00:00
|
|
|
"github.com/docker/distribution/registry/proxy/scheduler"
|
|
|
|
"github.com/docker/distribution/registry/storage"
|
|
|
|
"github.com/docker/distribution/registry/storage/cache/memory"
|
2015-09-18 23:11:35 +00:00
|
|
|
"github.com/docker/distribution/registry/storage/driver/filesystem"
|
2015-07-29 18:12:01 +00:00
|
|
|
"github.com/docker/distribution/registry/storage/driver/inmemory"
|
|
|
|
)
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
var sbsMu sync.Mutex
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
type statsBlobStore struct {
|
|
|
|
stats map[string]int
|
|
|
|
blobs distribution.BlobStore
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sbs statsBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["put"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
return sbs.blobs.Put(ctx, mediaType, p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sbs statsBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["get"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
return sbs.blobs.Get(ctx, dgst)
|
|
|
|
}
|
|
|
|
|
2016-01-13 19:44:42 +00:00
|
|
|
func (sbs statsBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["create"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2016-01-13 19:44:42 +00:00
|
|
|
return sbs.blobs.Create(ctx, options...)
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (sbs statsBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["resume"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
return sbs.blobs.Resume(ctx, id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sbs statsBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["open"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
return sbs.blobs.Open(ctx, dgst)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sbs statsBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["serveblob"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
return sbs.blobs.ServeBlob(ctx, w, r, dgst)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sbs statsBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
2015-09-18 23:11:35 +00:00
|
|
|
|
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["stat"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
return sbs.blobs.Stat(ctx, dgst)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sbs statsBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
sbs.stats["delete"]++
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
return sbs.blobs.Delete(ctx, dgst)
|
|
|
|
}
|
|
|
|
|
|
|
|
type testEnv struct {
|
2015-09-18 23:11:35 +00:00
|
|
|
numUnique int
|
|
|
|
inRemote []distribution.Descriptor
|
|
|
|
store proxyBlobStore
|
|
|
|
ctx context.Context
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
func (te *testEnv) LocalStats() *map[string]int {
|
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
ls := te.store.localStore.(statsBlobStore).stats
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
2015-07-29 18:12:01 +00:00
|
|
|
return &ls
|
|
|
|
}
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
func (te *testEnv) RemoteStats() *map[string]int {
|
|
|
|
sbsMu.Lock()
|
2015-07-29 18:12:01 +00:00
|
|
|
rs := te.store.remoteStore.(statsBlobStore).stats
|
2015-09-18 23:11:35 +00:00
|
|
|
sbsMu.Unlock()
|
2015-07-29 18:12:01 +00:00
|
|
|
return &rs
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate remote store and record the digests
|
2015-09-18 23:11:35 +00:00
|
|
|
func makeTestEnv(t *testing.T, name string) *testEnv {
|
2015-12-15 22:35:23 +00:00
|
|
|
nameRef, err := reference.ParseNamed(name)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unable to parse reference: %s", err)
|
|
|
|
}
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
truthDir, err := ioutil.TempDir("", "truth")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unable to create tempdir: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
cacheDir, err := ioutil.TempDir("", "cache")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unable to create tempdir: %s", err)
|
|
|
|
}
|
|
|
|
|
2016-04-26 21:36:38 +00:00
|
|
|
localDriver, err := filesystem.FromParameters(map[string]interface{}{
|
|
|
|
"rootdirectory": truthDir,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unable to create filesystem driver: %s", err)
|
|
|
|
}
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
// todo: create a tempfile area here
|
2016-04-26 21:36:38 +00:00
|
|
|
localRegistry, err := storage.NewRegistry(ctx, localDriver, storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableRedirect, storage.DisableDigestResumption)
|
2015-08-18 17:56:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating registry: %v", err)
|
|
|
|
}
|
2015-12-15 22:35:23 +00:00
|
|
|
localRepo, err := localRegistry.Repository(ctx, nameRef)
|
2015-07-29 18:12:01 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting repo: %v", err)
|
|
|
|
}
|
|
|
|
|
2016-04-26 21:36:38 +00:00
|
|
|
cacheDriver, err := filesystem.FromParameters(map[string]interface{}{
|
|
|
|
"rootdirectory": cacheDir,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unable to create filesystem driver: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
truthRegistry, err := storage.NewRegistry(ctx, cacheDriver, storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()))
|
2015-08-18 17:56:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating registry: %v", err)
|
|
|
|
}
|
2015-12-15 22:35:23 +00:00
|
|
|
truthRepo, err := truthRegistry.Repository(ctx, nameRef)
|
2015-07-29 18:12:01 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting repo: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
truthBlobs := statsBlobStore{
|
|
|
|
stats: make(map[string]int),
|
|
|
|
blobs: truthRepo.Blobs(ctx),
|
|
|
|
}
|
|
|
|
|
|
|
|
localBlobs := statsBlobStore{
|
|
|
|
stats: make(map[string]int),
|
|
|
|
blobs: localRepo.Blobs(ctx),
|
|
|
|
}
|
|
|
|
|
|
|
|
s := scheduler.New(ctx, inmemory.New(), "/scheduler-state.json")
|
|
|
|
|
|
|
|
proxyBlobStore := proxyBlobStore{
|
2016-01-27 00:42:10 +00:00
|
|
|
repositoryName: nameRef,
|
|
|
|
remoteStore: truthBlobs,
|
|
|
|
localStore: localBlobs,
|
|
|
|
scheduler: s,
|
2016-02-11 02:07:28 +00:00
|
|
|
authChallenger: &mockChallenger{},
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
te := &testEnv{
|
2015-07-29 18:12:01 +00:00
|
|
|
store: proxyBlobStore,
|
|
|
|
ctx: ctx,
|
|
|
|
}
|
|
|
|
return te
|
|
|
|
}
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
func makeBlob(size int) []byte {
|
|
|
|
blob := make([]byte, size, size)
|
|
|
|
for i := 0; i < size; i++ {
|
|
|
|
blob[i] = byte('A' + rand.Int()%48)
|
|
|
|
}
|
|
|
|
return blob
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
rand.Seed(42)
|
|
|
|
}
|
|
|
|
|
|
|
|
func perm(m []distribution.Descriptor) []distribution.Descriptor {
|
|
|
|
for i := 0; i < len(m); i++ {
|
|
|
|
j := rand.Intn(i + 1)
|
|
|
|
tmp := m[i]
|
|
|
|
m[i] = m[j]
|
|
|
|
m[j] = tmp
|
|
|
|
}
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
func populate(t *testing.T, te *testEnv, blobCount, size, numUnique int) {
|
2015-07-29 18:12:01 +00:00
|
|
|
var inRemote []distribution.Descriptor
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
for i := 0; i < numUnique; i++ {
|
|
|
|
bytes := makeBlob(size)
|
|
|
|
for j := 0; j < blobCount/numUnique; j++ {
|
|
|
|
desc, err := te.store.remoteStore.Put(te.ctx, "", bytes)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Put in store")
|
|
|
|
}
|
|
|
|
|
|
|
|
inRemote = append(inRemote, desc)
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
te.inRemote = inRemote
|
2015-09-18 23:11:35 +00:00
|
|
|
te.numUnique = numUnique
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
2016-02-18 00:32:23 +00:00
|
|
|
func TestProxyStoreGet(t *testing.T) {
|
|
|
|
te := makeTestEnv(t, "foo/bar")
|
|
|
|
|
|
|
|
localStats := te.LocalStats()
|
|
|
|
remoteStats := te.RemoteStats()
|
|
|
|
|
|
|
|
populate(t, te, 1, 10, 1)
|
|
|
|
_, err := te.store.Get(te.ctx, te.inRemote[0].Digest)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*localStats)["get"] != 1 && (*localStats)["put"] != 1 {
|
|
|
|
t.Errorf("Unexpected local counts")
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*remoteStats)["get"] != 1 {
|
|
|
|
t.Errorf("Unexpected remote get count")
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = te.store.Get(te.ctx, te.inRemote[0].Digest)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*localStats)["get"] != 2 && (*localStats)["put"] != 1 {
|
|
|
|
t.Errorf("Unexpected local counts")
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*remoteStats)["get"] != 1 {
|
|
|
|
t.Errorf("Unexpected remote get count")
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2015-07-29 18:12:01 +00:00
|
|
|
|
|
|
|
func TestProxyStoreStat(t *testing.T) {
|
|
|
|
te := makeTestEnv(t, "foo/bar")
|
2015-09-18 23:11:35 +00:00
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
remoteBlobCount := 1
|
2015-09-18 23:11:35 +00:00
|
|
|
populate(t, te, remoteBlobCount, 10, 1)
|
2015-07-29 18:12:01 +00:00
|
|
|
|
|
|
|
localStats := te.LocalStats()
|
|
|
|
remoteStats := te.RemoteStats()
|
|
|
|
|
|
|
|
// Stat - touches both stores
|
|
|
|
for _, d := range te.inRemote {
|
|
|
|
_, err := te.store.Stat(te.ctx, d.Digest)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error stating proxy store")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*localStats)["stat"] != remoteBlobCount {
|
|
|
|
t.Errorf("Unexpected local stat count")
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*remoteStats)["stat"] != remoteBlobCount {
|
|
|
|
t.Errorf("Unexpected remote stat count")
|
|
|
|
}
|
2016-02-11 02:07:28 +00:00
|
|
|
|
|
|
|
if te.store.authChallenger.(*mockChallenger).count != len(te.inRemote) {
|
|
|
|
t.Fatalf("Unexpected auth challenge count, got %#v", te.store.authChallenger)
|
|
|
|
}
|
|
|
|
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
func TestProxyStoreServeHighConcurrency(t *testing.T) {
|
2015-07-29 18:12:01 +00:00
|
|
|
te := makeTestEnv(t, "foo/bar")
|
2015-09-18 23:11:35 +00:00
|
|
|
blobSize := 200
|
|
|
|
blobCount := 10
|
|
|
|
numUnique := 1
|
|
|
|
populate(t, te, blobCount, blobSize, numUnique)
|
|
|
|
|
|
|
|
numClients := 16
|
|
|
|
testProxyStoreServe(t, te, numClients)
|
|
|
|
}
|
2015-07-29 18:12:01 +00:00
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
func TestProxyStoreServeMany(t *testing.T) {
|
|
|
|
te := makeTestEnv(t, "foo/bar")
|
|
|
|
blobSize := 200
|
|
|
|
blobCount := 10
|
|
|
|
numUnique := 4
|
|
|
|
populate(t, te, blobCount, blobSize, numUnique)
|
|
|
|
|
|
|
|
numClients := 4
|
|
|
|
testProxyStoreServe(t, te, numClients)
|
|
|
|
}
|
|
|
|
|
|
|
|
// todo(richardscothern): blobCount must be smaller than num clients
|
|
|
|
func TestProxyStoreServeBig(t *testing.T) {
|
|
|
|
te := makeTestEnv(t, "foo/bar")
|
|
|
|
|
|
|
|
blobSize := 2 << 20
|
|
|
|
blobCount := 4
|
|
|
|
numUnique := 2
|
|
|
|
populate(t, te, blobCount, blobSize, numUnique)
|
|
|
|
|
|
|
|
numClients := 4
|
|
|
|
testProxyStoreServe(t, te, numClients)
|
|
|
|
}
|
|
|
|
|
|
|
|
// testProxyStoreServe will create clients to consume all blobs
|
|
|
|
// populated in the truth store
|
|
|
|
func testProxyStoreServe(t *testing.T, te *testEnv, numClients int) {
|
2015-07-29 18:12:01 +00:00
|
|
|
localStats := te.LocalStats()
|
|
|
|
remoteStats := te.RemoteStats()
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
|
|
for i := 0; i < numClients; i++ {
|
|
|
|
// Serveblob - pulls through blobs
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
for _, remoteBlob := range te.inRemote {
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
r, err := http.NewRequest("GET", "", nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = te.store.ServeBlob(te.ctx, w, r, remoteBlob.Digest)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf(err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
bodyBytes := w.Body.Bytes()
|
2015-12-14 22:30:51 +00:00
|
|
|
localDigest := digest.FromBytes(bodyBytes)
|
2015-09-18 23:11:35 +00:00
|
|
|
if localDigest != remoteBlob.Digest {
|
|
|
|
t.Fatalf("Mismatching blob fetch from proxy")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
2015-07-29 18:12:01 +00:00
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
wg.Wait()
|
2015-07-29 18:12:01 +00:00
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
remoteBlobCount := len(te.inRemote)
|
2016-08-29 20:39:24 +00:00
|
|
|
sbsMu.Lock()
|
2015-09-18 23:11:35 +00:00
|
|
|
if (*localStats)["stat"] != remoteBlobCount*numClients && (*localStats)["create"] != te.numUnique {
|
2016-08-29 20:39:24 +00:00
|
|
|
sbsMu.Unlock()
|
2015-09-18 23:11:35 +00:00
|
|
|
t.Fatal("Expected: stat:", remoteBlobCount*numClients, "create:", remoteBlobCount)
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
2016-08-29 20:39:24 +00:00
|
|
|
sbsMu.Unlock()
|
2015-07-29 18:12:01 +00:00
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
// Wait for any async storage goroutines to finish
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
|
2016-08-29 20:39:24 +00:00
|
|
|
sbsMu.Lock()
|
2015-09-18 23:11:35 +00:00
|
|
|
remoteStatCount := (*remoteStats)["stat"]
|
|
|
|
remoteOpenCount := (*remoteStats)["open"]
|
2016-08-29 20:39:24 +00:00
|
|
|
sbsMu.Unlock()
|
2015-07-29 18:12:01 +00:00
|
|
|
|
|
|
|
// Serveblob - blobs come from local
|
|
|
|
for _, dr := range te.inRemote {
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
r, err := http.NewRequest("GET", "", nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = te.store.ServeBlob(te.ctx, w, r, dr.Digest)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf(err.Error())
|
|
|
|
}
|
|
|
|
|
2015-12-14 22:30:51 +00:00
|
|
|
dl := digest.FromBytes(w.Body.Bytes())
|
2015-07-29 18:12:01 +00:00
|
|
|
if dl != dr.Digest {
|
|
|
|
t.Errorf("Mismatching blob fetch from proxy")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
localStats = te.LocalStats()
|
|
|
|
remoteStats = te.RemoteStats()
|
2015-07-29 18:12:01 +00:00
|
|
|
|
2015-09-18 23:11:35 +00:00
|
|
|
// Ensure remote unchanged
|
2016-08-29 20:39:24 +00:00
|
|
|
sbsMu.Lock()
|
|
|
|
defer sbsMu.Unlock()
|
2015-09-18 23:11:35 +00:00
|
|
|
if (*remoteStats)["stat"] != remoteStatCount && (*remoteStats)["open"] != remoteOpenCount {
|
|
|
|
t.Fatalf("unexpected remote stats: %#v", remoteStats)
|
2015-07-29 18:12:01 +00:00
|
|
|
}
|
|
|
|
}
|