package ipfscluster import ( "context" "errors" "flag" "fmt" "math/rand" "mime/multipart" "os" "path/filepath" "sort" "strings" "sync" "testing" "time" "github.com/ipfs-cluster/ipfs-cluster/allocator/balanced" "github.com/ipfs-cluster/ipfs-cluster/api" "github.com/ipfs-cluster/ipfs-cluster/api/rest" "github.com/ipfs-cluster/ipfs-cluster/consensus/crdt" "github.com/ipfs-cluster/ipfs-cluster/consensus/raft" "github.com/ipfs-cluster/ipfs-cluster/datastore/badger" "github.com/ipfs-cluster/ipfs-cluster/datastore/inmem" "github.com/ipfs-cluster/ipfs-cluster/datastore/leveldb" "github.com/ipfs-cluster/ipfs-cluster/informer/disk" "github.com/ipfs-cluster/ipfs-cluster/ipfsconn/ipfshttp" "github.com/ipfs-cluster/ipfs-cluster/monitor/pubsubmon" "github.com/ipfs-cluster/ipfs-cluster/observations" "github.com/ipfs-cluster/ipfs-cluster/pintracker/stateless" "github.com/ipfs-cluster/ipfs-cluster/state" "github.com/ipfs-cluster/ipfs-cluster/test" "github.com/ipfs-cluster/ipfs-cluster/version" ds "github.com/ipfs/go-datastore" libp2p "github.com/libp2p/go-libp2p" dht "github.com/libp2p/go-libp2p-kad-dht" dual "github.com/libp2p/go-libp2p-kad-dht/dual" pubsub "github.com/libp2p/go-libp2p-pubsub" crypto "github.com/libp2p/go-libp2p/core/crypto" host "github.com/libp2p/go-libp2p/core/host" peer "github.com/libp2p/go-libp2p/core/peer" peerstore "github.com/libp2p/go-libp2p/core/peerstore" routedhost "github.com/libp2p/go-libp2p/p2p/host/routed" ma "github.com/multiformats/go-multiaddr" ) var ( // number of clusters to create nClusters = 5 // number of pins to pin/unpin/check nPins = 100 logLevel = "FATAL" customLogLvlFacilities = logFacilities{} consensus = "crdt" datastore = "badger" ttlDelayTime = 2 * time.Second // set on Main to diskInf.MetricTTL testsFolder = "clusterTestsFolder" // When testing with fixed ports... // clusterPort = 10000 // apiPort = 10100 // ipfsProxyPort = 10200 ) type logFacilities []string // String is the method to format the flag's value, part of the flag.Value interface. func (lg *logFacilities) String() string { return fmt.Sprint(*lg) } // Set is the method to set the flag value, part of the flag.Value interface. func (lg *logFacilities) Set(value string) error { if len(*lg) > 0 { return errors.New("logFacilities flag already set") } for _, lf := range strings.Split(value, ",") { *lg = append(*lg, lf) } return nil } // TestMain runs test initialization. Since Go1.13 we cannot run this on init() // as flag.Parse() does not work well there // (see https://golang.org/src/testing/testing.go#L211) func TestMain(m *testing.M) { rand.Seed(time.Now().UnixNano()) ReadyTimeout = 11 * time.Second // GossipSub needs to heartbeat to discover newly connected hosts // This speeds things up a little. pubsub.GossipSubHeartbeatInterval = 50 * time.Millisecond flag.Var(&customLogLvlFacilities, "logfacs", "use -logLevel for only the following log facilities; comma-separated") flag.StringVar(&logLevel, "loglevel", logLevel, "default log level for tests") flag.IntVar(&nClusters, "nclusters", nClusters, "number of clusters to use") flag.IntVar(&nPins, "npins", nPins, "number of pins to pin/unpin/check") flag.StringVar(&consensus, "consensus", consensus, "consensus implementation") flag.StringVar(&datastore, "datastore", datastore, "datastore backend") flag.Parse() if len(customLogLvlFacilities) <= 0 { for f := range LoggingFacilities { SetFacilityLogLevel(f, logLevel) } for f := range LoggingFacilitiesExtra { SetFacilityLogLevel(f, logLevel) } } for _, f := range customLogLvlFacilities { if _, ok := LoggingFacilities[f]; ok { SetFacilityLogLevel(f, logLevel) continue } if _, ok := LoggingFacilitiesExtra[f]; ok { SetFacilityLogLevel(f, logLevel) continue } } diskInfCfg := &disk.Config{} diskInfCfg.LoadJSON(testingDiskInfCfg) ttlDelayTime = diskInfCfg.MetricTTL * 2 os.Exit(m.Run()) } func randomBytes() []byte { bs := make([]byte, 64) for i := 0; i < len(bs); i++ { b := byte(rand.Int()) bs[i] = b } return bs } func createComponents( t *testing.T, host host.Host, pubsub *pubsub.PubSub, dht *dual.DHT, i int, staging bool, ) ( *Config, ds.Datastore, Consensus, []API, IPFSConnector, PinTracker, PeerMonitor, PinAllocator, Informer, Tracer, *test.IpfsMock, ) { ctx := context.Background() mock := test.NewIpfsMock(t) //apiAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", apiPort+i)) // Bind on port 0 apiAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") // Bind on Port 0 // proxyAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", ipfsProxyPort+i)) proxyAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") nodeAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", mock.Addr, mock.Port)) peername := fmt.Sprintf("peer_%d", i) ident, clusterCfg, apiCfg, ipfsproxyCfg, ipfshttpCfg, badgerCfg, levelDBCfg, raftCfg, crdtCfg, statelesstrackerCfg, psmonCfg, allocBalancedCfg, diskInfCfg, tracingCfg := testingConfigs() ident.ID = host.ID() ident.PrivateKey = host.Peerstore().PrivKey(host.ID()) clusterCfg.Peername = peername clusterCfg.LeaveOnShutdown = false clusterCfg.SetBaseDir(filepath.Join(testsFolder, host.ID().Pretty())) apiCfg.HTTPListenAddr = []ma.Multiaddr{apiAddr} ipfsproxyCfg.ListenAddr = []ma.Multiaddr{proxyAddr} ipfsproxyCfg.NodeAddr = nodeAddr ipfshttpCfg.NodeAddr = nodeAddr raftCfg.DataFolder = filepath.Join(testsFolder, host.ID().Pretty()) badgerCfg.Folder = filepath.Join(testsFolder, host.ID().Pretty(), "badger") levelDBCfg.Folder = filepath.Join(testsFolder, host.ID().Pretty(), "leveldb") api, err := rest.NewAPI(ctx, apiCfg) if err != nil { t.Fatal(err) } ipfsProxy, err := rest.NewAPI(ctx, apiCfg) if err != nil { t.Fatal(err) } ipfs, err := ipfshttp.NewConnector(ipfshttpCfg) if err != nil { t.Fatal(err) } alloc, err := balanced.New(allocBalancedCfg) if err != nil { t.Fatal(err) } inf, err := disk.NewInformer(diskInfCfg) if err != nil { t.Fatal(err) } store := makeStore(t, badgerCfg, levelDBCfg) cons := makeConsensus(t, store, host, pubsub, dht, raftCfg, staging, crdtCfg) tracker := stateless.New(statelesstrackerCfg, ident.ID, clusterCfg.Peername, cons.State) var peersF func(context.Context) ([]peer.ID, error) if consensus == "raft" { peersF = cons.Peers } mon, err := pubsubmon.New(ctx, psmonCfg, pubsub, peersF) if err != nil { t.Fatal(err) } tracingCfg.ServiceName = peername tracer, err := observations.SetupTracing(tracingCfg) if err != nil { t.Fatal(err) } return clusterCfg, store, cons, []API{api, ipfsProxy}, ipfs, tracker, mon, alloc, inf, tracer, mock } func makeStore(t *testing.T, badgerCfg *badger.Config, levelDBCfg *leveldb.Config) ds.Datastore { switch consensus { case "crdt": if datastore == "badger" { dstr, err := badger.New(badgerCfg) if err != nil { t.Fatal(err) } return dstr } dstr, err := leveldb.New(levelDBCfg) if err != nil { t.Fatal(err) } return dstr default: return inmem.New() } } func makeConsensus(t *testing.T, store ds.Datastore, h host.Host, psub *pubsub.PubSub, dht *dual.DHT, raftCfg *raft.Config, staging bool, crdtCfg *crdt.Config) Consensus { switch consensus { case "raft": raftCon, err := raft.NewConsensus(h, raftCfg, store, staging) if err != nil { t.Fatal(err) } return raftCon case "crdt": crdtCon, err := crdt.New(h, dht, psub, crdtCfg, store) if err != nil { t.Fatal(err) } return crdtCon default: panic("bad consensus") } } func createCluster(t *testing.T, host host.Host, dht *dual.DHT, clusterCfg *Config, store ds.Datastore, consensus Consensus, apis []API, ipfs IPFSConnector, tracker PinTracker, mon PeerMonitor, alloc PinAllocator, inf Informer, tracer Tracer) *Cluster { cl, err := NewCluster(context.Background(), host, dht, clusterCfg, store, consensus, apis, ipfs, tracker, mon, alloc, []Informer{inf}, tracer) if err != nil { t.Fatal(err) } return cl } func createOnePeerCluster(t *testing.T, nth int, clusterSecret []byte) (*Cluster, *test.IpfsMock) { hosts, pubsubs, dhts := createHosts(t, clusterSecret, 1) clusterCfg, store, consensus, api, ipfs, tracker, mon, alloc, inf, tracer, mock := createComponents(t, hosts[0], pubsubs[0], dhts[0], nth, false) cl := createCluster(t, hosts[0], dhts[0], clusterCfg, store, consensus, api, ipfs, tracker, mon, alloc, inf, tracer) <-cl.Ready() return cl, mock } func createHosts(t *testing.T, clusterSecret []byte, nClusters int) ([]host.Host, []*pubsub.PubSub, []*dual.DHT) { hosts := make([]host.Host, nClusters) pubsubs := make([]*pubsub.PubSub, nClusters) dhts := make([]*dual.DHT, nClusters) tcpaddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") quicAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/udp/0/quic") for i := range hosts { priv, _, err := crypto.GenerateKeyPair(crypto.RSA, 2048) if err != nil { t.Fatal(err) } h, p, d := createHost(t, priv, clusterSecret, []ma.Multiaddr{quicAddr, tcpaddr}) hosts[i] = h dhts[i] = d pubsubs[i] = p } return hosts, pubsubs, dhts } func createHost(t *testing.T, priv crypto.PrivKey, clusterSecret []byte, listen []ma.Multiaddr) (host.Host, *pubsub.PubSub, *dual.DHT) { ctx := context.Background() h, err := newHost(ctx, clusterSecret, priv, libp2p.ListenAddrs(listen...)) if err != nil { t.Fatal(err) } // DHT needs to be created BEFORE connecting the peers d, err := newTestDHT(ctx, h) if err != nil { t.Fatal(err) } // Pubsub needs to be created BEFORE connecting the peers, // otherwise they are not picked up. psub, err := newPubSub(ctx, h) if err != nil { t.Fatal(err) } return routedhost.Wrap(h, d), psub, d } func newTestDHT(ctx context.Context, h host.Host) (*dual.DHT, error) { return newDHT(ctx, h, nil, dual.DHTOption(dht.RoutingTableRefreshPeriod(600*time.Millisecond)), dual.DHTOption(dht.RoutingTableRefreshQueryTimeout(300*time.Millisecond)), ) } func createClusters(t *testing.T) ([]*Cluster, []*test.IpfsMock) { ctx := context.Background() os.RemoveAll(testsFolder) cfgs := make([]*Config, nClusters) stores := make([]ds.Datastore, nClusters) cons := make([]Consensus, nClusters) apis := make([][]API, nClusters) ipfss := make([]IPFSConnector, nClusters) trackers := make([]PinTracker, nClusters) mons := make([]PeerMonitor, nClusters) allocs := make([]PinAllocator, nClusters) infs := make([]Informer, nClusters) tracers := make([]Tracer, nClusters) ipfsMocks := make([]*test.IpfsMock, nClusters) clusters := make([]*Cluster, nClusters) // Uncomment when testing with fixed ports // clusterPeers := make([]ma.Multiaddr, nClusters, nClusters) hosts, pubsubs, dhts := createHosts(t, testingClusterSecret, nClusters) for i := 0; i < nClusters; i++ { // staging = true for all except first (i==0) cfgs[i], stores[i], cons[i], apis[i], ipfss[i], trackers[i], mons[i], allocs[i], infs[i], tracers[i], ipfsMocks[i] = createComponents(t, hosts[i], pubsubs[i], dhts[i], i, i != 0) } // Start first node clusters[0] = createCluster(t, hosts[0], dhts[0], cfgs[0], stores[0], cons[0], apis[0], ipfss[0], trackers[0], mons[0], allocs[0], infs[0], tracers[0]) <-clusters[0].Ready() bootstrapAddr := clusterAddr(clusters[0]) // Start the rest and join for i := 1; i < nClusters; i++ { clusters[i] = createCluster(t, hosts[i], dhts[i], cfgs[i], stores[i], cons[i], apis[i], ipfss[i], trackers[i], mons[i], allocs[i], infs[i], tracers[i]) err := clusters[i].Join(ctx, bootstrapAddr) if err != nil { logger.Error(err) t.Fatal(err) } <-clusters[i].Ready() } // connect all hosts for _, h := range hosts { for _, h2 := range hosts { if h.ID() != h2.ID() { h.Peerstore().AddAddrs(h2.ID(), h2.Addrs(), peerstore.PermanentAddrTTL) _, err := h.Network().DialPeer(ctx, h2.ID()) if err != nil { t.Log(err) } } } } waitForLeader(t, clusters) waitForClustersHealthy(t, clusters) return clusters, ipfsMocks } func shutdownClusters(t *testing.T, clusters []*Cluster, m []*test.IpfsMock) { for i, c := range clusters { shutdownCluster(t, c, m[i]) } os.RemoveAll(testsFolder) } func shutdownCluster(t *testing.T, c *Cluster, m *test.IpfsMock) { err := c.Shutdown(context.Background()) if err != nil { t.Error(err) } c.dht.Close() c.host.Close() c.datastore.Close() m.Close() } func collectGlobalPinInfos(t *testing.T, out <-chan api.GlobalPinInfo, timeout time.Duration) []api.GlobalPinInfo { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() var gpis []api.GlobalPinInfo for { select { case <-ctx.Done(): t.Error(ctx.Err()) return gpis case gpi, ok := <-out: if !ok { return gpis } gpis = append(gpis, gpi) } } } func collectPinInfos(t *testing.T, out <-chan api.PinInfo) []api.PinInfo { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var pis []api.PinInfo for { select { case <-ctx.Done(): t.Error(ctx.Err()) return pis case pi, ok := <-out: if !ok { return pis } pis = append(pis, pi) } } } func runF(t *testing.T, clusters []*Cluster, f func(*testing.T, *Cluster)) { t.Helper() var wg sync.WaitGroup for _, c := range clusters { wg.Add(1) go func(c *Cluster) { defer wg.Done() f(t, c) }(c) } wg.Wait() } // //////////////////////////////////// // Delay and wait functions // // Delays are used in tests to wait for certain events to happen: // - ttlDelay() waits for metrics to arrive. If you pin something // and your next operation depends on updated metrics, you need to wait // - pinDelay() accounts for the time necessary to pin something and for the new // log entry to be visible in all cluster peers // - delay just sleeps a second or two. // - waitForLeader functions make sure there is a raft leader, for example, // after killing the leader. // // The values for delays are a result of testing and adjusting so tests pass // in travis, jenkins etc., taking into account the values used in the // testing configuration (config_test.go). func delay() { var d int if nClusters > 10 { d = 3000 } else { d = 2000 } time.Sleep(time.Duration(d) * time.Millisecond) } func pinDelay() { time.Sleep(800 * time.Millisecond) } func ttlDelay() { time.Sleep(ttlDelayTime) } // Like waitForLeader but letting metrics expire before waiting, and // waiting for new metrics to arrive afterwards. func waitForLeaderAndMetrics(t *testing.T, clusters []*Cluster) { ttlDelay() waitForLeader(t, clusters) ttlDelay() } // Makes sure there is a leader and everyone knows about it. func waitForLeader(t *testing.T, clusters []*Cluster) { if consensus == "crdt" { return // yai } ctx := context.Background() timer := time.NewTimer(time.Minute) ticker := time.NewTicker(100 * time.Millisecond) loop: for { select { case <-timer.C: t.Fatal("timed out waiting for a leader") case <-ticker.C: for _, cl := range clusters { if cl.shutdownB { continue // skip shutdown clusters } _, err := cl.consensus.Leader(ctx) if err != nil { continue loop } } break loop } } } func waitForClustersHealthy(t *testing.T, clusters []*Cluster) { t.Helper() if len(clusters) == 0 { return } timer := time.NewTimer(15 * time.Second) for { ttlDelay() metrics := clusters[0].monitor.LatestMetrics(context.Background(), clusters[0].informers[0].Name()) healthy := 0 for _, m := range metrics { if !m.Expired() { healthy++ } } if len(clusters) == healthy { return } select { case <-timer.C: t.Fatal("timed out waiting for clusters to be healthy") default: } } } ///////////////////////////////////////// func TestClustersVersion(t *testing.T) { clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) f := func(t *testing.T, c *Cluster) { v := c.Version() if v != version.Version.String() { t.Error("Bad version") } } runF(t, clusters, f) } func TestClustersPeers(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) delay() j := rand.Intn(nClusters) // choose a random cluster peer out := make(chan api.ID, len(clusters)) clusters[j].Peers(ctx, out) if len(out) != nClusters { t.Fatal("expected as many peers as clusters") } clusterIDMap := make(map[peer.ID]api.ID) peerIDMap := make(map[peer.ID]api.ID) for _, c := range clusters { id := c.ID(ctx) clusterIDMap[id.ID] = id } for p := range out { if p.Error != "" { t.Error(p.ID, p.Error) continue } peerIDMap[p.ID] = p } for k, id := range clusterIDMap { id2, ok := peerIDMap[k] if !ok { t.Fatal("expected id in both maps") } //if !crypto.KeyEqual(id.PublicKey, id2.PublicKey) { // t.Error("expected same public key") //} if id.IPFS.ID != id2.IPFS.ID { t.Error("expected same ipfs daemon ID") } } } func TestClustersPin(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) prefix := test.Cid1.Prefix() ttlDelay() for i := 0; i < nPins; i++ { j := rand.Intn(nClusters) // choose a random cluster peer h, err := prefix.Sum(randomBytes()) // create random cid if err != nil { t.Fatal(err) } _, err = clusters[j].Pin(ctx, api.NewCid(h), api.PinOptions{}) if err != nil { t.Errorf("error pinning %s: %s", h, err) } // // Test re-pin // err = clusters[j].Pin(ctx, api.PinCid(h)) // if err != nil { // t.Errorf("error repinning %s: %s", h, err) // } } switch consensus { case "crdt": time.Sleep(10 * time.Second) default: delay() } fpinned := func(t *testing.T, c *Cluster) { out := make(chan api.PinInfo, 10) go func() { err := c.tracker.StatusAll(ctx, api.TrackerStatusUndefined, out) if err != nil { t.Error(err) } }() status := collectPinInfos(t, out) for _, v := range status { if v.Status != api.TrackerStatusPinned { t.Errorf("%s should have been pinned but it is %s", v.Cid, v.Status) } } if l := len(status); l != nPins { t.Errorf("Pinned %d out of %d requests", l, nPins) } } runF(t, clusters, fpinned) // Unpin everything pinList, err := clusters[0].pinsSlice(ctx) if err != nil { t.Fatal(err) } if len(pinList) != nPins { t.Fatalf("pin list has %d but pinned %d", len(pinList), nPins) } for i := 0; i < len(pinList); i++ { // test re-unpin fails j := rand.Intn(nClusters) // choose a random cluster peer _, err := clusters[j].Unpin(ctx, pinList[i].Cid) if err != nil { t.Errorf("error unpinning %s: %s", pinList[i].Cid, err) } } switch consensus { case "crdt": time.Sleep(10 * time.Second) default: delay() } for i := 0; i < len(pinList); i++ { j := rand.Intn(nClusters) // choose a random cluster peer _, err := clusters[j].Unpin(ctx, pinList[i].Cid) if err == nil { t.Errorf("expected error re-unpinning %s", pinList[i].Cid) } } delay() funpinned := func(t *testing.T, c *Cluster) { out := make(chan api.PinInfo) go func() { err := c.tracker.StatusAll(ctx, api.TrackerStatusUndefined, out) if err != nil { t.Error(err) } }() status := collectPinInfos(t, out) for _, v := range status { t.Errorf("%s should have been unpinned but it is %s", v.Cid, v.Status) } } runF(t, clusters, funpinned) } func TestClustersPinUpdate(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) prefix := test.Cid1.Prefix() ttlDelay() h, _ := prefix.Sum(randomBytes()) // create random cid h2, _ := prefix.Sum(randomBytes()) // create random cid _, err := clusters[0].PinUpdate(ctx, api.NewCid(h), api.NewCid(h2), api.PinOptions{}) if err == nil || err != state.ErrNotFound { t.Fatal("pin update should fail when from is not pinned") } _, err = clusters[0].Pin(ctx, api.NewCid(h), api.PinOptions{}) if err != nil { t.Errorf("error pinning %s: %s", h, err) } pinDelay() expiry := time.Now().AddDate(1, 0, 0) opts2 := api.PinOptions{ UserAllocations: []peer.ID{clusters[0].host.ID()}, // should not be used PinUpdate: api.NewCid(h), Name: "new name", ExpireAt: expiry, } _, err = clusters[0].Pin(ctx, api.NewCid(h2), opts2) // should call PinUpdate if err != nil { t.Errorf("error pin-updating %s: %s", h2, err) } pinDelay() f := func(t *testing.T, c *Cluster) { pinget, err := c.PinGet(ctx, api.NewCid(h2)) if err != nil { t.Fatal(err) } if len(pinget.Allocations) != 0 { t.Error("new pin should be allocated everywhere like pin1") } if pinget.MaxDepth != -1 { t.Error("updated pin should be recursive like pin1") } // We compare Unix seconds because our protobuf serde will have // lost any sub-second precision. if pinget.ExpireAt.Unix() != expiry.Unix() { t.Errorf("Expiry didn't match. Expected: %s. Got: %s", expiry, pinget.ExpireAt) } if pinget.Name != "new name" { t.Error("name should be kept") } } runF(t, clusters, f) } func TestClustersPinDirect(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) prefix := test.Cid1.Prefix() ttlDelay() h, _ := prefix.Sum(randomBytes()) // create random cid _, err := clusters[0].Pin(ctx, api.NewCid(h), api.PinOptions{Mode: api.PinModeDirect}) if err != nil { t.Fatal(err) } pinDelay() f := func(t *testing.T, c *Cluster, mode api.PinMode) { pinget, err := c.PinGet(ctx, api.NewCid(h)) if err != nil { t.Fatal(err) } if pinget.Mode != mode { t.Error("pin should be pinned in direct mode") } if pinget.MaxDepth != mode.ToPinDepth() { t.Errorf("pin should have max-depth %d but has %d", mode.ToPinDepth(), pinget.MaxDepth) } pInfo := c.StatusLocal(ctx, api.NewCid(h)) if pInfo.Error != "" { t.Error(pInfo.Error) } if pInfo.Status != api.TrackerStatusPinned { t.Error(pInfo.Error) t.Error("the status should show the hash as pinned") } } runF(t, clusters, func(t *testing.T, c *Cluster) { f(t, c, api.PinModeDirect) }) // Convert into a recursive mode _, err = clusters[0].Pin(ctx, api.NewCid(h), api.PinOptions{Mode: api.PinModeRecursive}) if err != nil { t.Fatal(err) } pinDelay() runF(t, clusters, func(t *testing.T, c *Cluster) { f(t, c, api.PinModeRecursive) }) // This should fail as we cannot convert back to direct _, err = clusters[0].Pin(ctx, api.NewCid(h), api.PinOptions{Mode: api.PinModeDirect}) if err == nil { t.Error("a recursive pin cannot be converted back to direct pin") } } func TestClustersStatusAll(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) h := test.Cid1 clusters[0].Pin(ctx, h, api.PinOptions{Name: "test"}) pinDelay() // Global status f := func(t *testing.T, c *Cluster) { out := make(chan api.GlobalPinInfo, 10) go func() { err := c.StatusAll(ctx, api.TrackerStatusUndefined, out) if err != nil { t.Error(err) } }() statuses := collectGlobalPinInfos(t, out, 5*time.Second) if len(statuses) != 1 { t.Fatal("bad status. Expected one item") } if !statuses[0].Cid.Equals(h) { t.Error("bad cid in status") } if statuses[0].Name != "test" { t.Error("globalPinInfo should have the name") } info := statuses[0].PeerMap if len(info) != nClusters { t.Error("bad info in status") } for _, pi := range info { if pi.IPFS != test.PeerID1 { t.Error("ipfs not set in pin status") } } pid := c.host.ID().String() if info[pid].Status != api.TrackerStatusPinned { t.Error("the hash should have been pinned") } status, err := c.Status(ctx, h) if err != nil { t.Error(err) } pinfo, ok := status.PeerMap[pid] if !ok { t.Fatal("Host not in status") } if pinfo.Status != api.TrackerStatusPinned { t.Error(pinfo.Error) t.Error("the status should show the hash as pinned") } } runF(t, clusters, f) } func TestClustersStatusAllWithErrors(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) h := test.Cid1 clusters[0].Pin(ctx, h, api.PinOptions{Name: "test"}) pinDelay() // shutdown 1 cluster peer clusters[1].Shutdown(ctx) clusters[1].host.Close() delay() f := func(t *testing.T, c *Cluster) { // skip if it's the shutdown peer if c.ID(ctx).ID == clusters[1].ID(ctx).ID { return } out := make(chan api.GlobalPinInfo, 10) go func() { err := c.StatusAll(ctx, api.TrackerStatusUndefined, out) if err != nil { t.Error(err) } }() statuses := collectGlobalPinInfos(t, out, 5*time.Second) if len(statuses) != 1 { t.Fatal("bad status. Expected one item") } if !statuses[0].Cid.Equals(h) { t.Error("wrong Cid in globalPinInfo") } if statuses[0].Name != "test" { t.Error("wrong Name in globalPinInfo") } // Raft and CRDT behave differently here switch consensus { case "raft": // Raft will have all statuses with one of them // being in ERROR because the peer is off stts := statuses[0] if len(stts.PeerMap) != nClusters { t.Error("bad number of peers in status") } pid := clusters[1].id.String() errst := stts.PeerMap[pid] if errst.Status != api.TrackerStatusClusterError { t.Error("erroring status should be set to ClusterError:", errst.Status) } if errst.PeerName != "peer_1" { t.Error("peername should have been set in the erroring peer too from the cache") } if errst.IPFS != test.PeerID1 { t.Error("IPFS ID should have been set in the erroring peer too from the cache") } // now check with Cid status status, err := c.Status(ctx, h) if err != nil { t.Error(err) } pinfo := status.PeerMap[pid] if pinfo.Status != api.TrackerStatusClusterError { t.Error("erroring status should be ClusterError:", pinfo.Status) } if pinfo.PeerName != "peer_1" { t.Error("peername should have been set in the erroring peer too from the cache") } if pinfo.IPFS != test.PeerID1 { t.Error("IPFS ID should have been set in the erroring peer too from the cache") } case "crdt": // CRDT will not have contacted the offline peer because // its metric expired and therefore is not in the // peerset. if len(statuses[0].PeerMap) != nClusters-1 { t.Error("expected a different number of statuses") } default: t.Fatal("bad consensus") } } runF(t, clusters, f) } func TestClustersRecoverLocal(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) h := test.ErrorCid // This cid always fails h2 := test.Cid2 ttlDelay() clusters[0].Pin(ctx, h, api.PinOptions{}) clusters[0].Pin(ctx, h2, api.PinOptions{}) pinDelay() pinDelay() f := func(t *testing.T, c *Cluster) { _, err := c.RecoverLocal(ctx, h) if err != nil { t.Fatal(err) } // Wait for queue to be processed delay() info := c.StatusLocal(ctx, h) if info.Status != api.TrackerStatusPinError { t.Errorf("element is %s and not PinError", info.Status) } // Recover good ID info, _ = c.RecoverLocal(ctx, h2) if info.Status != api.TrackerStatusPinned { t.Error("element should be in Pinned state") } } // Test Local syncs runF(t, clusters, f) } func TestClustersRecover(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) h := test.ErrorCid // This cid always fails h2 := test.Cid2 ttlDelay() clusters[0].Pin(ctx, h, api.PinOptions{}) clusters[0].Pin(ctx, h2, api.PinOptions{}) pinDelay() pinDelay() j := rand.Intn(nClusters) ginfo, err := clusters[j].Recover(ctx, h) if err != nil { // we always attempt to return a valid response // with errors contained in GlobalPinInfo t.Fatal("did not expect an error") } if len(ginfo.PeerMap) != nClusters { t.Error("number of peers do not match") } // Wait for queue to be processed delay() ginfo, err = clusters[j].Status(ctx, h) if err != nil { t.Fatal(err) } pinfo, ok := ginfo.PeerMap[clusters[j].host.ID().String()] if !ok { t.Fatal("should have info for this host") } if pinfo.Error == "" { t.Error("pinInfo error should not be empty") } for _, c := range clusters { inf, ok := ginfo.PeerMap[c.host.ID().String()] if !ok { t.Fatal("GlobalPinInfo should not be empty for this host") } if inf.Status != api.TrackerStatusPinError { t.Logf("%+v", inf) t.Error("should be PinError in all peers") } } // Test with a good Cid j = rand.Intn(nClusters) ginfo, err = clusters[j].Recover(ctx, h2) if err != nil { t.Fatal(err) } if !ginfo.Cid.Equals(h2) { t.Error("GlobalPinInfo should be for testrCid2") } if len(ginfo.PeerMap) != nClusters { t.Error("number of peers do not match") } for _, c := range clusters { inf, ok := ginfo.PeerMap[c.host.ID().String()] if !ok { t.Fatal("GlobalPinInfo should have this cluster") } if inf.Status != api.TrackerStatusPinned { t.Error("the GlobalPinInfo should show Pinned in all peers") } } } func TestClustersRecoverAll(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) h1 := test.Cid1 hError := test.ErrorCid ttlDelay() clusters[0].Pin(ctx, h1, api.PinOptions{}) clusters[0].Pin(ctx, hError, api.PinOptions{}) pinDelay() out := make(chan api.GlobalPinInfo) go func() { err := clusters[rand.Intn(nClusters)].RecoverAll(ctx, out) if err != nil { t.Error(err) } }() gInfos := collectGlobalPinInfos(t, out, 5*time.Second) if len(gInfos) != 1 { t.Error("expected one items") } for _, gInfo := range gInfos { if len(gInfo.PeerMap) != nClusters { t.Error("number of peers do not match") } } } func TestClustersShutdown(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) f := func(t *testing.T, c *Cluster) { err := c.Shutdown(ctx) if err != nil { t.Error("should be able to shutdown cleanly") } } // Shutdown 3 times runF(t, clusters, f) runF(t, clusters, f) runF(t, clusters, f) } func TestClustersReplicationOverall(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = nClusters - 1 c.config.ReplicationFactorMax = nClusters - 1 } // Why is replication factor nClusters - 1? // Because that way we know that pinning nCluster // pins with an strategy like numpins/disk // will result in each peer holding locally exactly // nCluster pins. prefix := test.Cid1.Prefix() for i := 0; i < nClusters; i++ { // Pick a random cluster and hash j := rand.Intn(nClusters) // choose a random cluster peer h, err := prefix.Sum(randomBytes()) // create random cid if err != nil { t.Fatal(err) } _, err = clusters[j].Pin(ctx, api.NewCid(h), api.PinOptions{}) if err != nil { t.Error(err) } pinDelay() // check that it is held by exactly nClusters - 1 peers gpi, err := clusters[j].Status(ctx, api.NewCid(h)) if err != nil { t.Fatal(err) } numLocal := 0 numRemote := 0 for _, v := range gpi.PeerMap { if v.Status == api.TrackerStatusPinned { numLocal++ } else if v.Status == api.TrackerStatusRemote { numRemote++ } } if numLocal != nClusters-1 { t.Errorf( "We wanted replication %d but it's only %d", nClusters-1, numLocal, ) } if numRemote != 1 { t.Errorf("We wanted 1 peer track as remote but %d do", numRemote) } ttlDelay() } f := func(t *testing.T, c *Cluster) { // confirm that the pintracker state matches the current global state out := make(chan api.PinInfo, 100) go func() { err := c.tracker.StatusAll(ctx, api.TrackerStatusUndefined, out) if err != nil { t.Error(err) } }() pinfos := collectPinInfos(t, out) if len(pinfos) != nClusters { t.Error("Pinfos does not have the expected pins") } numRemote := 0 numLocal := 0 for _, pi := range pinfos { switch pi.Status { case api.TrackerStatusPinned: numLocal++ case api.TrackerStatusRemote: numRemote++ } } if numLocal != nClusters-1 { t.Errorf("%s: Expected %d local pins but got %d", c.id.String(), nClusters-1, numLocal) } if numRemote != 1 { t.Errorf("%s: Expected 1 remote pin but got %d", c.id.String(), numRemote) } outPins := make(chan api.Pin) go func() { err := c.Pins(ctx, outPins) if err != nil { t.Error(err) } }() for pin := range outPins { allocs := pin.Allocations if len(allocs) != nClusters-1 { t.Errorf("Allocations are [%s]", allocs) } for _, a := range allocs { if a == c.id { pinfo := c.tracker.Status(ctx, pin.Cid) if pinfo.Status != api.TrackerStatusPinned { t.Errorf("Peer %s was allocated but it is not pinning cid", c.id) } } } } } runF(t, clusters, f) } // This test checks that we pin with ReplicationFactorMax when // we can func TestClustersReplicationFactorMax(t *testing.T) { ctx := context.Background() if nClusters < 3 { t.Skip("Need at least 3 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = 1 c.config.ReplicationFactorMax = nClusters - 1 } ttlDelay() h := test.Cid1 _, err := clusters[0].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } pinDelay() f := func(t *testing.T, c *Cluster) { p, err := c.PinGet(ctx, h) if err != nil { t.Fatal(err) } if len(p.Allocations) != nClusters-1 { t.Error("should have pinned nClusters - 1 allocations") } if p.ReplicationFactorMin != 1 { t.Error("rplMin should be 1") } if p.ReplicationFactorMax != nClusters-1 { t.Error("rplMax should be nClusters-1") } } runF(t, clusters, f) } // This tests checks that repinning something that is overpinned // removes some allocations func TestClustersReplicationFactorMaxLower(t *testing.T) { ctx := context.Background() if nClusters < 5 { t.Skip("Need at least 5 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = 1 c.config.ReplicationFactorMax = nClusters } ttlDelay() // make sure we have places to pin h := test.Cid1 _, err := clusters[0].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } pinDelay() p1, err := clusters[0].PinGet(ctx, h) if err != nil { t.Fatal(err) } if len(p1.Allocations) != nClusters { t.Fatal("allocations should be nClusters") } opts := api.PinOptions{ ReplicationFactorMin: 1, ReplicationFactorMax: 2, } _, err = clusters[0].Pin(ctx, h, opts) if err != nil { t.Fatal(err) } pinDelay() p2, err := clusters[0].PinGet(ctx, h) if err != nil { t.Fatal(err) } if len(p2.Allocations) != 2 { t.Fatal("allocations should have been reduced to 2") } } // This test checks that when not all nodes are available, // we pin in as many as we can aiming for ReplicationFactorMax func TestClustersReplicationFactorInBetween(t *testing.T) { ctx := context.Background() if nClusters < 5 { t.Skip("Need at least 5 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = 1 c.config.ReplicationFactorMax = nClusters } ttlDelay() // Shutdown two peers clusters[nClusters-1].Shutdown(ctx) clusters[nClusters-2].Shutdown(ctx) waitForLeaderAndMetrics(t, clusters) h := test.Cid1 _, err := clusters[0].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } pinDelay() f := func(t *testing.T, c *Cluster) { if c == clusters[nClusters-1] || c == clusters[nClusters-2] { return } p, err := c.PinGet(ctx, h) if err != nil { t.Fatal(err) } if len(p.Allocations) != nClusters-2 { t.Error("should have pinned nClusters-2 allocations") } if p.ReplicationFactorMin != 1 { t.Error("rplMin should be 1") } if p.ReplicationFactorMax != nClusters { t.Error("rplMax should be nClusters") } } runF(t, clusters, f) } // This test checks that we do not pin something for which // we cannot reach ReplicationFactorMin func TestClustersReplicationFactorMin(t *testing.T) { ctx := context.Background() if nClusters < 5 { t.Skip("Need at least 5 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = nClusters - 1 c.config.ReplicationFactorMax = nClusters } // Shutdown two peers clusters[nClusters-1].Shutdown(ctx) waitForLeaderAndMetrics(t, clusters) clusters[nClusters-2].Shutdown(ctx) waitForLeaderAndMetrics(t, clusters) h := test.Cid1 _, err := clusters[0].Pin(ctx, h, api.PinOptions{}) if err == nil { t.Error("Pin should have failed as rplMin cannot be satisfied") } t.Log(err) if !strings.Contains(err.Error(), "not enough peers to allocate CID") { t.Fatal(err) } } // This tests checks that repinning something that has becomed // underpinned actually changes nothing if it's sufficiently pinned func TestClustersReplicationMinMaxNoRealloc(t *testing.T) { ctx := context.Background() if nClusters < 5 { t.Skip("Need at least 5 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = 1 c.config.ReplicationFactorMax = nClusters } ttlDelay() h := test.Cid1 _, err := clusters[0].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } pinDelay() // Shutdown two peers clusters[nClusters-1].Shutdown(ctx) waitForLeaderAndMetrics(t, clusters) clusters[nClusters-2].Shutdown(ctx) waitForLeaderAndMetrics(t, clusters) _, err = clusters[0].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } pinDelay() p, err := clusters[0].PinGet(ctx, h) if err != nil { t.Fatal(err) } if len(p.Allocations) != nClusters { t.Error("allocations should still be nCluster even if not all available") } if p.ReplicationFactorMax != nClusters { t.Error("rplMax should have not changed") } } // This test checks that repinning something that has becomed // underpinned does re-allocations when it's not sufficiently // pinned anymore. // FIXME: The manual repin only works if the pin options changed. func TestClustersReplicationMinMaxRealloc(t *testing.T) { ctx := context.Background() if nClusters < 5 { t.Skip("Need at least 5 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = 3 c.config.ReplicationFactorMax = 4 } ttlDelay() // make sure metrics are in h := test.Cid1 _, err := clusters[0].Pin(ctx, h, api.PinOptions{ Name: "a", }) if err != nil { t.Fatal(err) } pinDelay() p, err := clusters[0].PinGet(ctx, h) if err != nil { t.Fatal(err) } firstAllocations := p.Allocations peerIDMap := make(map[peer.ID]*Cluster) for _, a := range clusters { peerIDMap[a.id] = a } // kill two of the allocations // Only the first allocated peer (or the second if the first is // alerting) will automatically repin. alloc1 := peerIDMap[firstAllocations[1]] alloc2 := peerIDMap[firstAllocations[2]] safePeer := peerIDMap[firstAllocations[0]] alloc1.Shutdown(ctx) alloc2.Shutdown(ctx) waitForLeaderAndMetrics(t, clusters) // Repin - (although this should have been taken of as alerts // happen for the shutdown nodes. We force re-allocation by // changing the name. _, err = safePeer.Pin(ctx, h, api.PinOptions{ Name: "b", }) if err != nil { t.Fatal(err) } pinDelay() p, err = safePeer.PinGet(ctx, h) if err != nil { t.Fatal(err) } secondAllocations := p.Allocations strings1 := api.PeersToStrings(firstAllocations) strings2 := api.PeersToStrings(secondAllocations) sort.Strings(strings1) sort.Strings(strings2) t.Logf("Allocs1: %s", strings1) t.Logf("Allocs2: %s", strings2) if fmt.Sprintf("%s", strings1) == fmt.Sprintf("%s", strings2) { t.Error("allocations should have changed") } lenSA := len(secondAllocations) expected := minInt(nClusters-2, 4) if lenSA != expected { t.Errorf("Insufficient reallocation, could have allocated to %d peers but instead only allocated to %d peers", expected, lenSA) } if lenSA < 3 { t.Error("allocations should be more than rplMin") } } // In this test we check that repinning something // when a node has gone down will re-assign the pin func TestClustersReplicationRealloc(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = nClusters - 1 c.config.ReplicationFactorMax = nClusters - 1 } ttlDelay() j := rand.Intn(nClusters) h := test.Cid1 _, err := clusters[j].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } // Let the pin arrive pinDelay() pinList, err := clusters[j].pinsSlice(ctx) if err != nil { t.Fatal(err) } pin := pinList[0] allocs := sort.StringSlice(api.PeersToStrings(pin.Allocations)) allocs.Sort() allocsStr := fmt.Sprintf("%s", allocs) // Re-pin should work and be allocated to the same // nodes _, err = clusters[j].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } pinDelay() pinList2, err := clusters[j].pinsSlice(ctx) if err != nil { t.Fatal(err) } pin2 := pinList2[0] allocs2 := sort.StringSlice(api.PeersToStrings(pin2.Allocations)) allocs2.Sort() allocsStr2 := fmt.Sprintf("%s", allocs2) if allocsStr != allocsStr2 { t.Fatal("allocations changed without reason") } //t.Log(allocsStr) //t.Log(allocsStr2) var killedClusterIndex int // find someone that pinned it and kill that cluster for i, c := range clusters { pinfo := c.tracker.Status(ctx, h) if pinfo.Status == api.TrackerStatusPinned { //t.Logf("Killing %s", c.id.Pretty()) killedClusterIndex = i t.Logf("Shutting down %s", c.ID(ctx).ID) c.Shutdown(ctx) break } } // let metrics expire and give time for the cluster to // see if they have lost the leader waitForLeaderAndMetrics(t, clusters) // Make sure we haven't killed our randomly // selected cluster for j == killedClusterIndex { j = rand.Intn(nClusters) } // now pin should succeed _, err = clusters[j].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } pinDelay() numPinned := 0 for i, c := range clusters { if i == killedClusterIndex { continue } pinfo := c.tracker.Status(ctx, h) if pinfo.Status == api.TrackerStatusPinned { //t.Log(pinfo.Peer.Pretty()) numPinned++ } } if numPinned != nClusters-1 { t.Error("pin should have been correctly re-assigned") } } // In this test we try to pin something when there are not // as many available peers a we need. It's like before, except // more peers are killed. func TestClustersReplicationNotEnoughPeers(t *testing.T) { ctx := context.Background() if nClusters < 5 { t.Skip("Need at least 5 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = nClusters - 1 c.config.ReplicationFactorMax = nClusters - 1 } ttlDelay() j := rand.Intn(nClusters) _, err := clusters[j].Pin(ctx, test.Cid1, api.PinOptions{}) if err != nil { t.Fatal(err) } // Let the pin arrive pinDelay() clusters[0].Shutdown(ctx) clusters[1].Shutdown(ctx) waitForLeaderAndMetrics(t, clusters) _, err = clusters[2].Pin(ctx, test.Cid2, api.PinOptions{}) if err == nil { t.Fatal("expected an error") } if !strings.Contains(err.Error(), "not enough peers to allocate") { t.Error("different error than expected") t.Error(err) } //t.Log(err) } func TestClustersRebalanceOnPeerDown(t *testing.T) { ctx := context.Background() if nClusters < 5 { t.Skip("Need at least 5 peers") } clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = nClusters - 1 c.config.ReplicationFactorMax = nClusters - 1 } // pin something h := test.Cid1 clusters[0].Pin(ctx, h, api.PinOptions{}) pinDelay() pinLocal := 0 pinRemote := 0 var localPinner string var remotePinner string var remotePinnerCluster *Cluster status, _ := clusters[0].Status(ctx, h) // check it was correctly pinned for p, pinfo := range status.PeerMap { if pinfo.Status == api.TrackerStatusPinned { pinLocal++ localPinner = p } else if pinfo.Status == api.TrackerStatusRemote { pinRemote++ remotePinner = p } } if pinLocal != nClusters-1 || pinRemote != 1 { t.Fatal("Not pinned as expected") } // kill the local pinner for _, c := range clusters { clid := c.id.String() if clid == localPinner { c.Shutdown(ctx) } else if clid == remotePinner { remotePinnerCluster = c } } delay() waitForLeaderAndMetrics(t, clusters) // in case we killed the leader // It should be now pinned in the remote pinner if s := remotePinnerCluster.tracker.Status(ctx, h).Status; s != api.TrackerStatusPinned { t.Errorf("it should be pinned and is %s", s) } } // Helper function for verifying cluster graph. Will only pass if exactly the // peers in clusterIDs are fully connected to each other and the expected ipfs // mock connectivity exists. Cluster peers not in clusterIDs are assumed to // be disconnected and the graph should reflect this func validateClusterGraph(t *testing.T, graph api.ConnectGraph, clusterIDs map[string]struct{}, peerNum int) { // Check that all cluster peers see each other as peers for id1, peers := range graph.ClusterLinks { if _, ok := clusterIDs[id1]; !ok { if len(peers) != 0 { t.Errorf("disconnected peer %s is still connected in graph", id1) } continue } t.Logf("id: %s, peers: %v\n", id1, peers) if len(peers) > len(clusterIDs)-1 { t.Errorf("More peers recorded in graph than expected") } // Make lookup index for peers connected to id1 peerIndex := make(map[string]struct{}) for _, p := range peers { peerIndex[p.String()] = struct{}{} } for id2 := range clusterIDs { if _, ok := peerIndex[id2]; id1 != id2 && !ok { t.Errorf("Expected graph to see peer %s connected to peer %s", id1, id2) } } } if len(graph.ClusterLinks) != peerNum { t.Errorf("Unexpected number of cluster nodes in graph") } // Check that all cluster peers are recorded as nodes in the graph for id := range clusterIDs { if _, ok := graph.ClusterLinks[id]; !ok { t.Errorf("Expected graph to record peer %s as a node", id) } } if len(graph.ClusterTrustLinks) != peerNum { t.Errorf("Unexpected number of trust links in graph") } // Check that the mocked ipfs swarm is recorded if len(graph.IPFSLinks) != 1 { t.Error("Expected exactly one ipfs peer for all cluster nodes, the mocked peer") } links, ok := graph.IPFSLinks[test.PeerID1.String()] if !ok { t.Error("Expected the mocked ipfs peer to be a node in the graph") } else { if len(links) != 2 || links[0] != test.PeerID4 || links[1] != test.PeerID5 { t.Error("Swarm peers of mocked ipfs are not those expected") } } // Check that the cluster to ipfs connections are all recorded for id := range clusterIDs { if ipfsID, ok := graph.ClustertoIPFS[id]; !ok { t.Errorf("Expected graph to record peer %s's ipfs connection", id) } else { if ipfsID != test.PeerID1 { t.Errorf("Unexpected error %s", ipfsID) } } } if len(graph.ClustertoIPFS) > len(clusterIDs) { t.Error("More cluster to ipfs links recorded in graph than expected") } } // In this test we get a cluster graph report from a random peer in a healthy // fully connected cluster and verify that it is formed as expected. func TestClustersGraphConnected(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) ttlDelay() j := rand.Intn(nClusters) // choose a random cluster peer to query graph, err := clusters[j].ConnectGraph() if err != nil { t.Fatal(err) } clusterIDs := make(map[string]struct{}) for _, c := range clusters { id := c.ID(ctx).ID.String() clusterIDs[id] = struct{}{} } validateClusterGraph(t, graph, clusterIDs, nClusters) } // Similar to the previous test we get a cluster graph report from a peer. // However now 2 peers have been shutdown and so we do not expect to see // them in the graph func TestClustersGraphUnhealthy(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) if nClusters < 5 { t.Skip("Need at least 5 peers") } j := rand.Intn(nClusters) // choose a random cluster peer to query // chose the clusters to shutdown discon1 := -1 discon2 := -1 for i := range clusters { if i != j { if discon1 == -1 { discon1 = i } else { discon2 = i break } } } clusters[discon1].Shutdown(ctx) clusters[discon1].host.Close() clusters[discon2].Shutdown(ctx) clusters[discon2].host.Close() waitForLeaderAndMetrics(t, clusters) graph, err := clusters[j].ConnectGraph() if err != nil { t.Fatal(err) } clusterIDs := make(map[string]struct{}) for i, c := range clusters { if i == discon1 || i == discon2 { continue } id := c.ID(ctx).ID.String() clusterIDs[id] = struct{}{} } peerNum := nClusters switch consensus { case "crdt": peerNum = nClusters - 2 } validateClusterGraph(t, graph, clusterIDs, peerNum) } // Check that the pin is not re-assigned when a node // that has disabled repinning goes down. func TestClustersDisabledRepinning(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) for _, c := range clusters { c.config.ReplicationFactorMin = nClusters - 1 c.config.ReplicationFactorMax = nClusters - 1 c.config.DisableRepinning = true } ttlDelay() j := rand.Intn(nClusters) h := test.Cid1 _, err := clusters[j].Pin(ctx, h, api.PinOptions{}) if err != nil { t.Fatal(err) } // Let the pin arrive pinDelay() var killedClusterIndex int // find someone that pinned it and kill that cluster for i, c := range clusters { pinfo := c.tracker.Status(ctx, h) if pinfo.Status == api.TrackerStatusPinned { killedClusterIndex = i t.Logf("Shutting down %s", c.ID(ctx).ID) c.Shutdown(ctx) break } } // let metrics expire and give time for the cluster to // see if they have lost the leader waitForLeaderAndMetrics(t, clusters) // Make sure we haven't killed our randomly // selected cluster for j == killedClusterIndex { j = rand.Intn(nClusters) } numPinned := 0 for i, c := range clusters { if i == killedClusterIndex { continue } pinfo := c.tracker.Status(ctx, h) if pinfo.Status == api.TrackerStatusPinned { //t.Log(pinfo.Peer.Pretty()) numPinned++ } } if numPinned != nClusters-2 { t.Errorf("expected %d replicas for pin, got %d", nClusters-2, numPinned) } } func TestRepoGC(t *testing.T) { clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) f := func(t *testing.T, c *Cluster) { gRepoGC, err := c.RepoGC(context.Background()) if err != nil { t.Fatal("gc should have worked:", err) } if gRepoGC.PeerMap == nil { t.Fatal("expected a non-nil peer map") } if len(gRepoGC.PeerMap) != nClusters { t.Errorf("expected repo gc information for %d peer", nClusters) } for _, repoGC := range gRepoGC.PeerMap { testRepoGC(t, repoGC) } } runF(t, clusters, f) } func TestClustersFollowerMode(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) _, err := clusters[0].Pin(ctx, test.Cid1, api.PinOptions{}) if err != nil { t.Fatal(err) } _, err = clusters[0].Pin(ctx, test.ErrorCid, api.PinOptions{}) if err != nil { t.Fatal(err) } // Let the pins arrive pinDelay() // Set Cluster1 to follower mode clusters[1].config.FollowerMode = true t.Run("follower cannot pin", func(t *testing.T) { _, err := clusters[1].PinPath(ctx, "/ipfs/"+test.Cid2.String(), api.PinOptions{}) if err != errFollowerMode { t.Error("expected follower mode error") } _, err = clusters[1].Pin(ctx, test.Cid2, api.PinOptions{}) if err != errFollowerMode { t.Error("expected follower mode error") } }) t.Run("follower cannot unpin", func(t *testing.T) { _, err := clusters[1].UnpinPath(ctx, "/ipfs/"+test.Cid1.String()) if err != errFollowerMode { t.Error("expected follower mode error") } _, err = clusters[1].Unpin(ctx, test.Cid1) if err != errFollowerMode { t.Error("expected follower mode error") } }) t.Run("follower cannot add", func(t *testing.T) { sth := test.NewShardingTestHelper() defer sth.Clean(t) params := api.DefaultAddParams() params.Shard = false params.Name = "testlocal" mfr, closer := sth.GetTreeMultiReader(t) defer closer.Close() r := multipart.NewReader(mfr, mfr.Boundary()) _, err = clusters[1].AddFile(ctx, r, params) if err != errFollowerMode { t.Error("expected follower mode error") } }) t.Run("follower status itself only", func(t *testing.T) { gpi, err := clusters[1].Status(ctx, test.Cid1) if err != nil { t.Error("status should work") } if len(gpi.PeerMap) != 1 { t.Fatal("globalPinInfo should only have one peer") } }) } func TestClusterPinsWithExpiration(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) ttlDelay() cl := clusters[rand.Intn(nClusters)] // choose a random cluster peer to query c := test.Cid1 expireIn := 1 * time.Second opts := api.PinOptions{ ExpireAt: time.Now().Add(expireIn), } _, err := cl.Pin(ctx, c, opts) if err != nil { t.Fatal("pin should have worked:", err) } pinDelay() pins, err := cl.pinsSlice(ctx) if err != nil { t.Fatal(err) } if len(pins) != 1 { t.Error("pin should be part of the state") } // wait till expiry time time.Sleep(expireIn) // manually call state sync on all peers, so we don't have to wait till // state sync interval for _, c := range clusters { err = c.StateSync(ctx) if err != nil { t.Error(err) } } pinDelay() // state sync should have unpinned expired pin pins, err = cl.pinsSlice(ctx) if err != nil { t.Fatal(err) } if len(pins) != 0 { t.Error("pin should not be part of the state") } } func TestClusterAlerts(t *testing.T) { ctx := context.Background() clusters, mock := createClusters(t) defer shutdownClusters(t, clusters, mock) if len(clusters) < 2 { t.Skip("need at least 2 nodes for this test") } ttlDelay() for _, c := range clusters[1:] { c.Shutdown(ctx) } ttlDelay() alerts := clusters[0].Alerts() if len(alerts) == 0 { t.Error("expected at least one alert") } }