// Copyright 2017 Pilosa Corp. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ctl import ( "context" "encoding/binary" "fmt" "hash/fnv" "io" "io/ioutil" "os" "path/filepath" "sort" "strconv" "strings" "syscall" "text/tabwriter" "time" "github.com/gogo/protobuf/proto" "github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2/internal" "github.com/pilosa/pilosa/v2/roaring" "github.com/pkg/errors" ) // InspectCommand represents a command for inspecting fragment data files. type InspectCommand struct { // Path to data file Path string // don't list details of objects Quiet bool // list only this many objects Max int // Filters: InspectOpts pilosa.InspectRequest // Standard input/output *pilosa.CmdIO } // NewInspectCommand returns a new instance of InspectCommand. func NewInspectCommand(stdin io.Reader, stdout, stderr io.Writer) *InspectCommand { return &InspectCommand{ CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr), } } type pointerContext struct { from, to uintptr } func (p *pointerContext) pretty(c roaring.ContainerInfo) string { var pointer string if c.Mapped { if c.Pointer >= p.from && c.Pointer < p.to { pointer = fmt.Sprintf("@+0x%x", c.Pointer-p.from) } else { pointer = fmt.Sprintf("!0x%x!", c.Pointer) } } else { pointer = fmt.Sprintf("0x%x", c.Pointer) } return fmt.Sprintf("%s \t%d \t%d \t%s ", c.Type, c.N, c.Alloc, pointer) } func (cmd *InspectCommand) PrintOps(info roaring.BitmapInfo) { fmt.Fprintln(cmd.Stdout, " Ops:") tw := tabwriter.NewWriter(cmd.Stdout, 0, 8, 0, '\t', 0) fmt.Fprintf(tw, " \t%s\t%s\t%s\t\n", "TYPE", "OpN", "SIZE") printed := 0 for _, op := range info.OpDetails { fmt.Fprintf(tw, "\t%s\t%d\t%d\t\n", op.Type, op.OpN, op.Size) printed++ if cmd.Max != 0 && printed >= cmd.Max { break } } tw.Flush() } func (cmd *InspectCommand) PrintContainers(info roaring.BitmapInfo, pC pointerContext) { fmt.Fprintln(cmd.Stdout, " Containers:") tw := tabwriter.NewWriter(cmd.Stdout, 0, 8, 0, '\t', 0) fmt.Fprintf(tw, " \t\tRoaring\t\t\t\tOps\t\t\t\tFlags\t\n") fmt.Fprintf(tw, "\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "KEY", "TYPE", "N", "ALLOC", "OFFSET", "TYPE", "N", "ALLOC", "OFFSET", "FLAGS") c1s := info.Containers c2s := info.OpContainers l1 := len(c1s) l2 := len(c2s) i1 := 0 i2 := 0 var c1, c2 roaring.ContainerInfo c1.Key = ^uint64(0) c2.Key = ^uint64(0) c1e := false c2e := false if i1 < l1 { c1 = c1s[i1] i1++ c1e = true } if i2 < l2 { c2 = c2s[i2] i2++ c2e = true } printed := 0 for c1e || c2e { c1used := false c2used := false var key uint64 c1fmt := "-\t\t\t" c2fmt := "-\t\t\t" // If c2 exists, we'll always prefer its flags, // if it doesn't, this gets overwritten. flags := c2.Flags if !c2e || (c1e && c1.Key < c2.Key) { c1fmt = pC.pretty(c1) key = c1.Key c1used = true flags = c1.Flags } else if !c1e || (c2e && c2.Key < c1.Key) { c2fmt = pC.pretty(c2) key = c2.Key c2used = true } else { // c1e and c2e both set, and neither key is < the other. c1fmt = pC.pretty(c1) c2fmt = pC.pretty(c2) key = c1.Key c1used = true c2used = true } if c1used { if i1 < l1 { c1 = c1s[i1] i1++ } else { c1e = false } } if c2used { if i2 < l2 { c2 = c2s[i2] i2++ } else { c2e = false } } fmt.Fprintf(tw, "\t%d\t%s\t%s\t%s\t\n", key, c1fmt, c2fmt, flags) printed++ if cmd.Max > 0 && printed >= cmd.Max { break } } tw.Flush() } // Run executes the inspect command. func (cmd *InspectCommand) Run(ctx context.Context) error { // Open file handle. f, err := os.Open(cmd.Path) if err != nil { return errors.Wrap(err, "opening file") } defer f.Close() fi, err := f.Stat() if err != nil { return errors.Wrap(err, "statting file") } if fi.IsDir() { total := 0 infos, err := f.Readdir(0) if err != nil { return err } if len(infos) == 0 { return errors.New("directory contains no files") } names := make([]string, len(infos)) nameToInfo := make(map[string]os.FileInfo, len(infos)) // find numeric-only names; we'll operate on // either those, or the whole holder if we find // a .topology file. n := 0 for _, fi := range infos { name := fi.Name() if name == ".topology" { return cmd.InspectHolder(ctx, cmd.Path) } if _, err := strconv.Atoi(name); err == nil { names[n] = name nameToInfo[name] = fi n++ } } if n == 0 { return fmt.Errorf("directory contains no fragments (looking for numeric names)") } names = names[:n] fmt.Fprintf(cmd.Stdout, "%s contains %d fragments:\n", cmd.Path, n) for _, name := range names { f2, err := os.Open(filepath.Join(cmd.Path, name)) if err != nil { return fmt.Errorf("opening %q: %v", name, err) } fmt.Fprintf(cmd.Stdout, "%s/%s:\n", cmd.Path, name) err = cmd.InspectFile(f2, nameToInfo[name]) total++ f2.Close() if err != nil { return fmt.Errorf("inspecting %q: %v", name, err) } } return nil } return cmd.InspectFile(f, fi) } // loadTopology is copied almost exactly from pilosa/cluster.go. func loadTopology(path string) (topology internal.Topology, myID string, err error) { buf, err := ioutil.ReadFile(filepath.Join(path, ".topology")) if os.IsNotExist(err) { return topology, myID, err } else if err != nil { return topology, myID, errors.Wrap(err, "reading file") } if err := proto.Unmarshal(buf, &topology); err != nil { return topology, myID, errors.Wrap(err, "unmarshalling") } sort.Slice(topology.NodeIDs, func(i, j int) bool { return topology.NodeIDs[i] < topology.NodeIDs[j] }) buf, err = ioutil.ReadFile(filepath.Join(path, ".id")) if os.IsNotExist(err) { return topology, myID, err } else if err != nil { return topology, myID, nil } myID = strings.TrimSpace(string(buf)) return topology, myID, nil } var partitions = make(map[string]map[uint64]int) func findPartition(index string, shard uint64, partitionN int) (partition int) { var shardMap map[uint64]int var ok bool if shardMap, ok = partitions[index]; !ok { shardMap = make(map[uint64]int) partitions[index] = shardMap } if partition, ok = shardMap[shard]; !ok { var buf [8]byte binary.BigEndian.PutUint64(buf[:], shard) // Hash the bytes and mod by partition count. h := fnv.New64a() _, _ = h.Write([]byte(index)) _, _ = h.Write(buf[:]) partition = int(h.Sum64() % uint64(partitionN)) shardMap[shard] = partition } return partition } func findPartitionPath(path string, partitionN int) (int, error) { parts := strings.Split(path, "/") shard, err := strconv.ParseUint(parts[len(parts)-1], 10, 64) if err != nil { return 0, err } return findPartition(parts[0], shard, partitionN), nil } func (cmd *InspectCommand) InspectHolder(ctx context.Context, path string) error { holder := pilosa.NewHolder(path, nil) holder.Opts.Inspect = true holder.Opts.ReadOnly = true err := holder.Open() if err != nil { return fmt.Errorf("%s: holder open: %v", path, err) } holderInfo, err := holder.Inspect(ctx, &cmd.InspectOpts) if err != nil { return fmt.Errorf("%s: inspect: %v", path, err) } myPartition := 0 topology, myID, err := loadTopology(path) if err == nil { fmt.Fprintf(cmd.Stdout, "Cluster ID: %q\n", topology.ClusterID) if len(topology.NodeIDs) > 1 { fmt.Fprintf(cmd.Stdout, "Cluster of %d nodes, this node %q\n", len(topology.NodeIDs), myID) } else { fmt.Fprintf(cmd.Stdout, "Cluster has only one node: %q\n", myID) } found := false for i := range topology.NodeIDs { if topology.NodeIDs[i] == myID { found = true myPartition = i break } } if !found { fmt.Fprintf(cmd.Stdout, "Warning: node ID %q not found in topology (%q)\n", myID, topology.NodeIDs) } } else { fmt.Fprintf(cmd.Stdout, "warning: reading topology failed: %v\n", err) } for _, name := range holderInfo.FragmentNames { partition, err := findPartitionPath(name, len(topology.NodeIDs)) if err != nil { fmt.Fprintf(cmd.Stdout, "%s: [can't find partition: %v]\n", name, err) } else { if partition == myPartition { fmt.Fprintf(cmd.Stdout, "%s:\n", name) } else { fmt.Fprintf(cmd.Stdout, "%s: [primary node %q]\n", name, topology.NodeIDs[partition]) } } details := holderInfo.FragmentInfo[name] cmd.DisplayInfo(details.BitmapInfo) if details.BlockChecksums != nil { fmt.Fprintf(cmd.Stdout, " Checksums [%d total]:\n", len(details.BlockChecksums)) for _, block := range details.BlockChecksums { fmt.Fprintf(cmd.Stdout, " %8d: %x\n", block.ID, block.Checksum) } } } return nil } func (cmd *InspectCommand) InspectFile(f *os.File, fi os.FileInfo) error { // Memory map the file. data, err := syscall.Mmap(int(f.Fd()), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED) if err != nil { return errors.Wrap(err, "mmapping") } defer func() { err := syscall.Munmap(data) if err != nil { fmt.Fprintf(cmd.Stderr, "inspect command: munmap failed: %v", err) } }() // Attach the mmap file to the bitmap. t := time.Now() fmt.Fprintf(cmd.Stderr, "inspecting bitmap...") var info roaring.BitmapInfo _, _, err = roaring.InspectBinary(data, true, &info) fmt.Fprintf(cmd.Stderr, " (%s)\n", time.Since(t)) cmd.DisplayInfo(info) if err != nil { return errors.Wrap(err, "inspecting") } return nil } func (cmd *InspectCommand) DisplayInfo(info roaring.BitmapInfo) { pC := pointerContext{ from: info.From, to: info.To, } // Print top-level info. fmt.Fprintf(cmd.Stdout, " Bitmap Info:\n") fmt.Fprintf(cmd.Stdout, " Bits: %d\n", info.BitCount) fmt.Fprintf(cmd.Stdout, " Containers: %d (%d roaring)\n", info.ContainerCount, len(info.Containers)) fmt.Fprintf(cmd.Stdout, " Operations: %d (%d bits)\n", info.Ops, info.OpN) fmt.Fprintln(cmd.Stdout, "") // Print info for each container. if !cmd.Quiet { if info.ContainerCount > 0 { cmd.PrintContainers(info, pC) } if info.Ops > 0 { cmd.PrintOps(info) } } }