featurebase/ctl/inspect.go
Seebs 4d494f6699
shared/generic functionality for iterating holders
This is sort of large, but it's annoyingly difficult to
separate out.

The basic idea is to allow us to have a single holder-iterating
block of code, which is associated with the holder, that can be used
for various things, like the snapshot queue background scan, or
for inspect operations.

We invent the concept of a HolderFilter, which is a thing that
can decide what things in a holder it cares about, and a HolderOperator,
which can also process those things selectively.

In the process, we fix up a couple of subtle bugs in the
inspect logic; specifically, the assumption that the mapped flag could
tell you whether a container was modified by the ops log doesn't
work with mmap, so we have a shiny new flag which is used to track
that, internal to the roaring/container code.

All of this leads to the actual *point* of this exercise, which is
making it easier to create an /inspect endpoint which produces almost
the same data we'd have gotten from `pilosa inspect` on a data directory;
the distinction is that it doesn't try to identify the distinction
between data from disk and data from operations since the file was
loaded. Possibly it should, but it doesn't yet.

The snapshot queue is now implemented using the HolderOperator
design, which requires some subtle changes to how it works, but
overall makes it easier to follow the snapshot queue logic,
and also shares that logic with the way Inspect works.

The holder's snapshot queue is now provided by the server, in
a default environment.

The queueless snapshot queue no longer triggers snapshots on
enqueue -- it turns out that breaks badly, because a key
point about enqueueing a snapshot is that it's safe to do it
*during* a transaction on that fragment, and triggering a
snapshot during a transaction actually causes horrible errors
as the ops log ends up being the old file, which we close.
Related to this, we also need to prevent closed fragments from
trying to snapshot, so we track fragment openness when opening
or closing, and bail on trying to snapshot a fragment which is closed.

We also stop using the queueless snapshot queue during tests,
because that's a horrible idea.

We copy a little bit of the partition logic from the cluster code so
we don't have to expose it all, this lets us check whether the node
we're looking at is the one which should be primary for a given shard,
and if not, identify which node would be. This works only when
pointed at a data directory, for now.

The test cases for the holder have to be internal, because pilosa
doesn't export view/fragment, just Index/Field. This means that the
holder test cases can't just use the test/* package, so they duplicate
some of its logic, approximately.
2020-06-29 15:18:47 -04:00

399 lines
10 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"
"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(pilosa.DefaultPartitionN)
holder.Path = path
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)
}
}
}