package topology import ( "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" "github.com/seaweedfs/seaweedfs/weed/storage/needle" ) // StreamVolumes hands every volume the filter selects to send, in batches, so // that neither end holds the whole cluster to move it. Batches are built under // their disk's lock and handed over outside it, so a slow reader stalls the // stream rather than the topology. // // Only the disks `listed` names are streamed. That listing went out first, and // the topology is walked again here, so without it a disk registering in // between would have its volumes sent to a client with nowhere to put them. // Bounding the walk by what was already announced makes the two agree by // construction: a disk arriving mid-listing is in neither, and is reported by // the next one. // // The batches still do not share one instant. Neither did a single listing, // which takes each disk's lock in turn, so a volume that moves while either is // running can be seen twice or not at all. func (t *Topology) StreamVolumes(listed *master_pb.TopologyInfo, filter VolumeFilter, batchSize int, send func(*master_pb.VolumeListStreamResponse) error) error { if batchSize <= 0 { batchSize = defaultVolumeStreamBatch } announced := announcedDisks(listed) for _, dcNode := range t.Children() { dc := dcNode.(*DataCenter) for _, rackNode := range dc.Children() { rack := rackNode.(*Rack) for _, dnNode := range rack.Children() { dn := dnNode.(*DataNode) for _, diskNode := range dn.Children() { disk := diskNode.(*Disk) if !announced[[4]string{string(dc.Id()), string(rack.Id()), string(dn.Id()), string(disk.Id())}] { continue } batch := func() *master_pb.VolumeListStreamResponse { return &master_pb.VolumeListStreamResponse{ DataCenter: string(dc.Id()), Rack: string(rack.Id()), DataNode: string(dn.Id()), DiskType: string(disk.Id()), } } if err := disk.streamVolumes(filter, batchSize, batch, send); err != nil { return err } if err := disk.streamEcShards(filter, batchSize, batch, send); err != nil { return err } } } } } return nil } const defaultVolumeStreamBatch = 10000 // announcedDisks names the disks a listing carried. func announcedDisks(listed *master_pb.TopologyInfo) map[[4]string]bool { announced := make(map[[4]string]bool) if listed == nil { return announced } for _, dc := range listed.DataCenterInfos { for _, rack := range dc.RackInfos { for _, node := range rack.DataNodeInfos { for diskType := range node.DiskInfos { announced[[4]string{dc.Id, rack.Id, node.Id, diskType}] = true } } } } return announced } // streamVolumes sends this disk's volumes a batch at a time. The ids are taken // in one pass and the messages built in later ones, so the lock is held for a // batch rather than for the disk, and only 4 bytes per volume are carried // between passes. A volume that leaves in between is simply not sent. func (d *Disk) streamVolumes(filter VolumeFilter, batchSize int, newBatch func() *master_pb.VolumeListStreamResponse, send func(*master_pb.VolumeListStreamResponse) error) error { d.RLock() ids := make([]needle.VolumeId, 0, len(d.volumes)) for id := range d.volumes { ids = append(ids, id) } d.RUnlock() for start := 0; start < len(ids); start += batchSize { end := min(start+batchSize, len(ids)) batch := newBatch() d.RLock() for _, id := range ids[start:end] { v, found := d.volumes[id] if !found || !filter.matches(v) { continue } batch.VolumeInfos = append(batch.VolumeInfos, v.ToVolumeInformationMessage()) } d.RUnlock() if len(batch.VolumeInfos) == 0 { continue } if err := send(batch); err != nil { return err } } return nil } func (d *Disk) streamEcShards(filter VolumeFilter, batchSize int, newBatch func() *master_pb.VolumeListStreamResponse, send func(*master_pb.VolumeListStreamResponse) error) error { // GetEcShards already copies under the lock, and a cluster holds far fewer // ec shards than volumes, so these only need cutting into batches. shards := d.GetEcShards() for start := 0; start < len(shards); start += batchSize { end := min(start+batchSize, len(shards)) batch := newBatch() for _, ecv := range shards[start:end] { if !filter.matches(ecv) { continue } batch.EcShardInfos = append(batch.EcShardInfos, ecv.ToVolumeEcShardInformationMessage()) } if len(batch.EcShardInfos) == 0 { continue } if err := send(batch); err != nil { return err } } return nil }