mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
When unmarshalling ops, we weren't adding a meaningful OpN to them, resulting in misleading reports from `pilosa inspect`. Also, we were mistakenly reporting things as "mapped" when they were actually using their internal storage (as with small array containers). Add the "sanity check" to `pilosa inspect` so that errors like the above get noticed more easily and corrected. Also, to make that work, have roaring.InspectBinary actually put containers in the bitmap it creates rather than just creating info entries for them.
407 lines
11 KiB
Go
407 lines
11 KiB
Go
// 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"
|
|
"unsafe"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/pilosa/pilosa/v2"
|
|
"github.com/pilosa/pilosa/v2/pb"
|
|
"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 pb.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)
|
|
}
|
|
}()
|
|
mappedFrom := uintptr(unsafe.Pointer(&data[0]))
|
|
mappedTo := mappedFrom + uintptr(len(data))
|
|
// Attach the mmap file to the bitmap.
|
|
t := time.Now()
|
|
fmt.Fprintf(cmd.Stderr, "inspecting bitmap...")
|
|
var info roaring.BitmapInfo
|
|
bitmap, _, 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")
|
|
}
|
|
mappedIn, mappedOut, unmappedIn, errs, err := bitmap.SanityCheckMapping(mappedFrom, mappedTo)
|
|
if err != nil {
|
|
fmt.Fprintf(cmd.Stderr, "sanity check: %d mapped in, %d mapped out, %d unmapped in, %d errors\n",
|
|
mappedIn, mappedOut, unmappedIn, errs)
|
|
fmt.Fprintf(cmd.Stderr, "last error: %v\n", err)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|