mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
needed an unlocked version of sendsync for use within the cluster, so also implemented that. Added a number of tests trying to reproduce the issue, but was not able to. Not sure it's worth keeping the new tests.
805 lines
20 KiB
Go
805 lines
20 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 pilosa
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pilosa/pilosa/logger"
|
|
"github.com/pilosa/pilosa/roaring"
|
|
"github.com/pilosa/pilosa/stats"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// Default server settings.
|
|
const (
|
|
defaultDiagnosticServer = "https://diagnostics.pilosa.com/v0/diagnostics"
|
|
)
|
|
|
|
// Ensure Server implements interfaces.
|
|
var _ broadcaster = &Server{}
|
|
|
|
// Server represents a holder wrapped by a running HTTP server.
|
|
type Server struct { // nolint: maligned
|
|
// Close management.
|
|
wg sync.WaitGroup
|
|
closing chan struct{}
|
|
|
|
// Internal
|
|
holder *Holder
|
|
cluster *cluster
|
|
diagnostics *diagnosticsCollector
|
|
executor *executor
|
|
hosts []string
|
|
clusterDisabled bool
|
|
serializer Serializer
|
|
|
|
// External
|
|
systemInfo SystemInfo
|
|
gcNotifier GCNotifier
|
|
logger logger.Logger
|
|
|
|
nodeID string
|
|
uri URI
|
|
antiEntropyInterval time.Duration
|
|
metricInterval time.Duration
|
|
diagnosticInterval time.Duration
|
|
maxWritesPerRequest int
|
|
isCoordinator bool
|
|
syncer holderSyncer
|
|
|
|
defaultClient InternalClient
|
|
dataDir string
|
|
}
|
|
|
|
// TODO: have this return an interface for Holder instead of concrete object?
|
|
func (s *Server) Holder() *Holder {
|
|
return s.holder
|
|
}
|
|
|
|
// ServerOption is a functional option type for pilosa.Server
|
|
type ServerOption func(s *Server) error
|
|
|
|
func OptServerLogger(l logger.Logger) ServerOption {
|
|
return func(s *Server) error {
|
|
s.logger = l
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerReplicaN(n int) ServerOption {
|
|
return func(s *Server) error {
|
|
s.cluster.ReplicaN = n
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerDataDir(dir string) ServerOption {
|
|
return func(s *Server) error {
|
|
s.dataDir = dir
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerAttrStoreFunc(af func(string) AttrStore) ServerOption {
|
|
return func(s *Server) error {
|
|
s.holder.NewAttrStore = af
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerAntiEntropyInterval(interval time.Duration) ServerOption {
|
|
return func(s *Server) error {
|
|
s.antiEntropyInterval = interval
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerLongQueryTime(dur time.Duration) ServerOption {
|
|
return func(s *Server) error {
|
|
s.cluster.longQueryTime = dur
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerMaxWritesPerRequest(n int) ServerOption {
|
|
return func(s *Server) error {
|
|
s.maxWritesPerRequest = n
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerMetricInterval(dur time.Duration) ServerOption {
|
|
return func(s *Server) error {
|
|
s.metricInterval = dur
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerSystemInfo(si SystemInfo) ServerOption {
|
|
return func(s *Server) error {
|
|
s.systemInfo = si
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerGCNotifier(gcn GCNotifier) ServerOption {
|
|
return func(s *Server) error {
|
|
s.gcNotifier = gcn
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerInternalClient(c InternalClient) ServerOption {
|
|
return func(s *Server) error {
|
|
s.executor = newExecutor(optExecutorInternalQueryClient(c))
|
|
s.defaultClient = c
|
|
s.cluster.InternalClient = c
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// DEPRECATED
|
|
func OptServerPrimaryTranslateStore(store TranslateStore) ServerOption {
|
|
return func(s *Server) error {
|
|
s.logger.Printf("DEPRECATED: OptServerPrimaryTranslateStore")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerPrimaryTranslateStoreFunc(tf func(interface{}) TranslateStore) ServerOption {
|
|
|
|
return func(s *Server) error {
|
|
s.holder.NewPrimaryTranslateStore = tf
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerStatsClient(sc stats.StatsClient) ServerOption {
|
|
return func(s *Server) error {
|
|
s.holder.Stats = sc
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerDiagnosticsInterval(dur time.Duration) ServerOption {
|
|
return func(s *Server) error {
|
|
s.diagnosticInterval = dur
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerURI(uri *URI) ServerOption {
|
|
return func(s *Server) error {
|
|
s.uri = *uri
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// OptClusterDisabled tells the server whether to use a static cluster with the
|
|
// defined hosts. Mostly used for testing.
|
|
func OptServerClusterDisabled(disabled bool, hosts []string) ServerOption {
|
|
return func(s *Server) error {
|
|
s.hosts = hosts
|
|
s.clusterDisabled = disabled
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerSerializer(ser Serializer) ServerOption {
|
|
return func(s *Server) error {
|
|
s.serializer = ser
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerIsCoordinator(is bool) ServerOption {
|
|
return func(s *Server) error {
|
|
s.isCoordinator = is
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerNodeID(nodeID string) ServerOption {
|
|
return func(s *Server) error {
|
|
s.nodeID = nodeID
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerClusterHasher(h Hasher) ServerOption {
|
|
return func(s *Server) error {
|
|
s.cluster.Hasher = h
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func OptServerTranslateFileMapSize(mapSize int) ServerOption {
|
|
return func(s *Server) error {
|
|
s.holder.translateFile = NewTranslateFile(OptTranslateFileMapSize(mapSize))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NewServer returns a new instance of Server.
|
|
func NewServer(opts ...ServerOption) (*Server, error) {
|
|
s := &Server{
|
|
closing: make(chan struct{}),
|
|
cluster: newCluster(),
|
|
holder: NewHolder(),
|
|
diagnostics: newDiagnosticsCollector(defaultDiagnosticServer),
|
|
systemInfo: newNopSystemInfo(),
|
|
defaultClient: nopInternalClient{},
|
|
|
|
gcNotifier: NopGCNotifier,
|
|
|
|
antiEntropyInterval: time.Minute * 10,
|
|
metricInterval: 0,
|
|
diagnosticInterval: 0,
|
|
|
|
logger: logger.NopLogger,
|
|
}
|
|
s.executor = newExecutor(optExecutorInternalQueryClient(s.defaultClient))
|
|
s.cluster.InternalClient = s.defaultClient
|
|
|
|
s.diagnostics.server = s
|
|
|
|
for _, opt := range opts {
|
|
err := opt(s)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "applying option")
|
|
}
|
|
}
|
|
s.holder.translateFile.logger = s.logger
|
|
|
|
path, err := expandDirName(s.dataDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.holder.Path = path
|
|
s.holder.translateFile.Path = filepath.Join(path, ".keys")
|
|
s.holder.Logger = s.logger
|
|
s.holder.Stats.SetLogger(s.logger)
|
|
|
|
s.cluster.Path = path
|
|
s.cluster.logger = s.logger
|
|
s.cluster.holder = s.holder
|
|
|
|
// Get or create NodeID.
|
|
s.nodeID = s.loadNodeID()
|
|
if s.isCoordinator {
|
|
s.cluster.Coordinator = s.nodeID
|
|
}
|
|
|
|
// Set Cluster Node.
|
|
node := &Node{
|
|
ID: s.nodeID,
|
|
URI: s.uri,
|
|
IsCoordinator: s.cluster.Coordinator == s.nodeID,
|
|
State: nodeStateDown,
|
|
}
|
|
s.cluster.Node = node
|
|
if s.clusterDisabled {
|
|
err := s.cluster.setStatic(s.hosts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "setting cluster static")
|
|
}
|
|
}
|
|
|
|
// Append the NodeID tag to stats.
|
|
s.holder.Stats = s.holder.Stats.WithTags(fmt.Sprintf("NodeID:%s", s.nodeID))
|
|
|
|
s.executor.Holder = s.holder
|
|
s.executor.Node = node
|
|
s.executor.Cluster = s.cluster
|
|
s.executor.TranslateStore = s.holder.translateFile
|
|
s.executor.MaxWritesPerRequest = s.maxWritesPerRequest
|
|
s.cluster.broadcaster = s
|
|
s.cluster.maxWritesPerRequest = s.maxWritesPerRequest
|
|
s.holder.broadcaster = s
|
|
s.holder.shardValidatorFunc = func(index string, shard uint64) bool {
|
|
return s.cluster.ownsShard(s.nodeID, index, shard)
|
|
}
|
|
|
|
err = s.cluster.setup()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "setting up cluster")
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Open opens and initializes the server.
|
|
func (s *Server) Open() error {
|
|
s.logger.Printf("open server")
|
|
|
|
// Log startup
|
|
err := s.holder.logStartup()
|
|
if err != nil {
|
|
log.Println(errors.Wrap(err, "logging startup"))
|
|
}
|
|
|
|
// Initialize id-key storage.
|
|
if err := s.holder.translateFile.Open(); err != nil {
|
|
return errors.Wrap(err, "opening TranslateFile")
|
|
}
|
|
|
|
// Open Cluster management.
|
|
if err := s.cluster.waitForStarted(); err != nil {
|
|
return errors.Wrap(err, "opening Cluster")
|
|
}
|
|
|
|
// Open holder.
|
|
if err := s.holder.Open(); err != nil {
|
|
return errors.Wrap(err, "opening Holder")
|
|
}
|
|
if err := s.cluster.setNodeState(nodeStateReady); err != nil {
|
|
return errors.Wrap(err, "setting nodeState")
|
|
}
|
|
|
|
// Listen for joining nodes.
|
|
// This needs to start after the Holder has opened so that nodes can join
|
|
// the cluster without waiting for data to load on the coordinator. Before
|
|
// this starts, the joins are queued up in the Cluster.joiningLeavingNodes
|
|
// buffered channel.
|
|
s.cluster.listenForJoins()
|
|
|
|
s.syncer.Holder = s.holder
|
|
s.syncer.Node = s.cluster.Node
|
|
s.syncer.Cluster = s.cluster
|
|
s.syncer.Closing = s.closing
|
|
s.syncer.Stats = s.holder.Stats.WithTags("HolderSyncer")
|
|
|
|
// Start background monitoring.
|
|
s.wg.Add(3)
|
|
go func() { defer s.wg.Done(); s.monitorAntiEntropy() }()
|
|
go func() { defer s.wg.Done(); s.monitorRuntime() }()
|
|
go func() { defer s.wg.Done(); s.monitorDiagnostics() }()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes the server and waits for it to shutdown.
|
|
func (s *Server) Close() error {
|
|
// Notify goroutines to stop.
|
|
close(s.closing)
|
|
s.wg.Wait()
|
|
|
|
var errh error
|
|
var errc error
|
|
if s.cluster != nil {
|
|
errc = s.cluster.close()
|
|
}
|
|
if s.holder != nil {
|
|
errh = s.holder.Close()
|
|
}
|
|
// prefer to return holder error over cluster
|
|
// error. This order is somewhat arbitrary. It would be better if we had
|
|
// some way to combine all the errors, but probably not important enough to
|
|
// warrant the extra complexity.
|
|
if errh != nil {
|
|
return errors.Wrap(errh, "closing holder")
|
|
}
|
|
return errors.Wrap(errc, "closing cluster")
|
|
}
|
|
|
|
// loadNodeID gets NodeID from disk, or creates a new value.
|
|
// If server.NodeID is already set, a new ID is not created.
|
|
func (s *Server) loadNodeID() string {
|
|
if s.nodeID != "" {
|
|
return s.nodeID
|
|
}
|
|
nodeID, err := s.holder.loadNodeID()
|
|
if err != nil {
|
|
s.logger.Printf("loading NodeID: %v", err)
|
|
return s.nodeID
|
|
}
|
|
return nodeID
|
|
}
|
|
|
|
// NodeID returns the server's node id.
|
|
func (s *Server) NodeID() string { return s.nodeID }
|
|
|
|
// SyncData manually invokes the anti entropy process which makes sure that this
|
|
// node has the data from all replicas across the cluster.
|
|
func (s *Server) SyncData() error {
|
|
return errors.Wrap(s.syncer.SyncHolder(), "syncing holder")
|
|
}
|
|
|
|
func (s *Server) monitorAntiEntropy() {
|
|
if s.antiEntropyInterval == 0 || s.cluster.ReplicaN <= 1 {
|
|
return // anti entropy disabled
|
|
}
|
|
s.cluster.initializeAntiEntropy()
|
|
|
|
ticker := time.NewTicker(s.antiEntropyInterval)
|
|
defer ticker.Stop()
|
|
|
|
s.logger.Printf("holder sync monitor initializing (%s interval)", s.antiEntropyInterval)
|
|
|
|
// Initialize syncer with local holder and remote client.
|
|
for {
|
|
// Wait for tick or a close.
|
|
select {
|
|
case <-s.closing:
|
|
return
|
|
case <-s.cluster.abortAntiEntropyCh: // receive here so we don't block resizing
|
|
continue
|
|
case <-ticker.C:
|
|
s.holder.Stats.Count("AntiEntropy", 1, 1.0)
|
|
}
|
|
t := time.Now()
|
|
if s.cluster.State() == ClusterStateResizing {
|
|
continue // don't launch anti-entropy during resize.
|
|
// the cluster sets its state to resizing and *then* sends to
|
|
// abortAntiEntropyCh before starting to resize
|
|
}
|
|
// Sync holders.
|
|
s.logger.Printf("holder sync beginning")
|
|
if err := s.syncer.SyncHolder(); err != nil {
|
|
s.logger.Printf("holder sync error: err=%s", err)
|
|
continue
|
|
}
|
|
|
|
// Record successful sync in log.
|
|
s.logger.Printf("holder sync complete")
|
|
dif := time.Since(t)
|
|
s.holder.Stats.Histogram("AntiEntropyDuration", float64(dif), 1.0)
|
|
|
|
// Drain tick channel since we just finished anti-entropy. If the AE
|
|
// process took a long time, we don't want them to pile up on each
|
|
// other.
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
continue
|
|
default:
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// receiveMessage represents an implementation of BroadcastHandler.
|
|
func (s *Server) receiveMessage(m Message) error {
|
|
switch obj := m.(type) {
|
|
case *CreateShardMessage:
|
|
f := s.holder.Field(obj.Index, obj.Field)
|
|
if f == nil {
|
|
return fmt.Errorf("local field not found: %s/%s", obj.Index, obj.Field)
|
|
}
|
|
if err := f.AddRemoteAvailableShards(roaring.NewBitmap(obj.Shard)); err != nil {
|
|
return errors.Wrap(err, "adding remote available shards")
|
|
}
|
|
case *CreateIndexMessage:
|
|
opt := obj.Meta
|
|
_, err := s.holder.CreateIndex(obj.Index, *opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *DeleteIndexMessage:
|
|
if err := s.holder.DeleteIndex(obj.Index); err != nil {
|
|
return err
|
|
}
|
|
case *CreateFieldMessage:
|
|
idx := s.holder.Index(obj.Index)
|
|
if idx == nil {
|
|
return fmt.Errorf("local index not found: %s", obj.Index)
|
|
}
|
|
opt := obj.Meta
|
|
_, err := idx.createField(obj.Field, *opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *DeleteFieldMessage:
|
|
idx := s.holder.Index(obj.Index)
|
|
if err := idx.DeleteField(obj.Field); err != nil {
|
|
return err
|
|
}
|
|
case *DeleteAvailableShardMessage:
|
|
f := s.holder.Field(obj.Index, obj.Field)
|
|
if err := f.RemoveAvailableShard(obj.ShardID); err != nil {
|
|
return err
|
|
}
|
|
case *CreateViewMessage:
|
|
f := s.holder.Field(obj.Index, obj.Field)
|
|
if f == nil {
|
|
return fmt.Errorf("local field not found: %s", obj.Field)
|
|
}
|
|
_, _, err := f.createViewIfNotExistsBase(obj.View)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *DeleteViewMessage:
|
|
f := s.holder.Field(obj.Index, obj.Field)
|
|
if f == nil {
|
|
return fmt.Errorf("local field not found: %s", obj.Field)
|
|
}
|
|
err := f.deleteView(obj.View)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *ClusterStatus:
|
|
err := s.cluster.mergeClusterStatus(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *ResizeInstruction:
|
|
err := s.cluster.followResizeInstruction(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *ResizeInstructionComplete:
|
|
err := s.cluster.markResizeInstructionComplete(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *SetCoordinatorMessage:
|
|
s.cluster.setCoordinator(obj.New)
|
|
case *UpdateCoordinatorMessage:
|
|
s.cluster.updateCoordinator(obj.New)
|
|
case *NodeStateMessage:
|
|
err := s.cluster.receiveNodeState(obj.NodeID, obj.State)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case *RecalculateCaches:
|
|
s.holder.recalculateCaches()
|
|
case *NodeEvent:
|
|
err := s.cluster.ReceiveEvent(obj)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "cluster receiving NodeEvent %v", obj)
|
|
}
|
|
case *NodeStatus:
|
|
s.handleRemoteStatus(obj)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SendSync represents an implementation of Broadcaster.
|
|
func (s *Server) SendSync(m Message) error {
|
|
var eg errgroup.Group
|
|
msg, err := s.serializer.Marshal(m)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling message: %v", err)
|
|
}
|
|
msg = append([]byte{getMessageType(m)}, msg...)
|
|
|
|
for _, node := range s.cluster.Nodes() {
|
|
node := node
|
|
// Don't forward the message to ourselves.
|
|
if s.uri == node.URI {
|
|
continue
|
|
}
|
|
|
|
eg.Go(func() error {
|
|
return s.defaultClient.SendMessage(context.Background(), &node.URI, msg)
|
|
})
|
|
}
|
|
|
|
return eg.Wait()
|
|
}
|
|
|
|
// SendAsync represents an implementation of Broadcaster.
|
|
func (s *Server) SendAsync(m Message) error {
|
|
return ErrNotImplemented
|
|
}
|
|
|
|
// SendTo represents an implementation of Broadcaster.
|
|
func (s *Server) SendTo(to *Node, m Message) error {
|
|
msg, err := s.serializer.Marshal(m)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling message: %v", err)
|
|
}
|
|
msg = append([]byte{getMessageType(m)}, msg...)
|
|
return s.defaultClient.SendMessage(context.Background(), &to.URI, msg)
|
|
}
|
|
|
|
// node returns the pilosa.node object. It is used by membership protocols to
|
|
// get this node's name(ID), location(URI), and coordinator status.
|
|
func (s *Server) node() Node {
|
|
return *s.cluster.Node
|
|
}
|
|
|
|
// handleRemoteStatus receives incoming NodeStatus from remote nodes.
|
|
func (s *Server) handleRemoteStatus(pb Message) {
|
|
// Ignore NodeStatus messages until the cluster is in a Normal state.
|
|
if s.cluster.State() != ClusterStateNormal {
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
// Make sure the holder has opened.
|
|
s.holder.opened.Recv()
|
|
|
|
err := s.mergeRemoteStatus(pb.(*NodeStatus))
|
|
if err != nil {
|
|
s.logger.Printf("merge remote status: %s", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *Server) mergeRemoteStatus(ns *NodeStatus) error {
|
|
// Ignore status updates from self.
|
|
if s.nodeID == ns.Node.ID {
|
|
return nil
|
|
}
|
|
|
|
// Sync schema.
|
|
if err := s.holder.applySchema(ns.Schema); err != nil {
|
|
return errors.Wrap(err, "applying schema")
|
|
}
|
|
|
|
// Sync available shards.
|
|
for _, is := range ns.Indexes {
|
|
for _, fs := range is.Fields {
|
|
f := s.holder.Field(is.Name, fs.Name)
|
|
|
|
// if we don't know about a field locally, log an error because
|
|
// fields should be created and synced prior to shard creation
|
|
if f == nil {
|
|
s.logger.Printf("local field not found: %s/%s", is.Name, fs.Name)
|
|
continue
|
|
}
|
|
if err := f.AddRemoteAvailableShards(fs.AvailableShards); err != nil {
|
|
return errors.Wrap(err, "adding remote available shards")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// monitorDiagnostics periodically polls the Pilosa Indexes for cluster info.
|
|
func (s *Server) monitorDiagnostics() {
|
|
// Do not send more than once a minute
|
|
if s.diagnosticInterval < time.Minute {
|
|
s.logger.Printf("diagnostics disabled")
|
|
return
|
|
} else {
|
|
s.logger.Printf("Pilosa is currently configured to send small diagnostics reports to our team every %v. More information here: https://www.pilosa.com/docs/latest/administration/#diagnostics", s.diagnosticInterval)
|
|
}
|
|
|
|
s.diagnostics.Logger = s.logger
|
|
s.diagnostics.SetVersion(Version)
|
|
s.diagnostics.Set("Host", s.uri.Host)
|
|
s.diagnostics.Set("Cluster", strings.Join(s.cluster.nodeIDs(), ","))
|
|
s.diagnostics.Set("NumNodes", len(s.cluster.nodes))
|
|
s.diagnostics.Set("NumCPU", runtime.NumCPU())
|
|
s.diagnostics.Set("NodeID", s.nodeID)
|
|
s.diagnostics.Set("ClusterID", s.cluster.id)
|
|
s.diagnostics.EnrichWithCPUInfo()
|
|
s.diagnostics.EnrichWithOSInfo()
|
|
|
|
// Flush the diagnostics metrics at startup, then on each tick interval
|
|
flush := func() {
|
|
openFiles, err := countOpenFiles()
|
|
if err == nil {
|
|
s.diagnostics.Set("OpenFiles", openFiles)
|
|
}
|
|
s.diagnostics.Set("GoRoutines", runtime.NumGoroutine())
|
|
s.diagnostics.EnrichWithMemoryInfo()
|
|
s.diagnostics.EnrichWithSchemaProperties()
|
|
s.diagnostics.CheckVersion()
|
|
err = s.diagnostics.Flush()
|
|
if err != nil {
|
|
s.logger.Printf("diagnostics error: %s", err)
|
|
}
|
|
}
|
|
|
|
ticker := time.NewTicker(s.diagnosticInterval)
|
|
defer ticker.Stop()
|
|
flush()
|
|
for {
|
|
// Wait for tick or a close.
|
|
select {
|
|
case <-s.closing:
|
|
return
|
|
case <-ticker.C:
|
|
flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
// monitorRuntime periodically polls the Go runtime metrics.
|
|
func (s *Server) monitorRuntime() {
|
|
// Disable metrics when poll interval is zero.
|
|
if s.metricInterval <= 0 {
|
|
return
|
|
}
|
|
|
|
var m runtime.MemStats
|
|
ticker := time.NewTicker(s.metricInterval)
|
|
defer ticker.Stop()
|
|
|
|
defer s.gcNotifier.Close()
|
|
|
|
s.logger.Printf("runtime stats initializing (%s interval)", s.metricInterval)
|
|
|
|
for {
|
|
// Wait for tick or a close.
|
|
select {
|
|
case <-s.closing:
|
|
return
|
|
case <-s.gcNotifier.AfterGC():
|
|
// GC just ran.
|
|
s.holder.Stats.Count("garbage_collection", 1, 1.0)
|
|
case <-ticker.C:
|
|
}
|
|
|
|
// Record the number of go routines.
|
|
s.holder.Stats.Gauge("goroutines", float64(runtime.NumGoroutine()), 1.0)
|
|
|
|
openFiles, err := countOpenFiles()
|
|
// Open File handles.
|
|
if err == nil {
|
|
s.holder.Stats.Gauge("OpenFiles", float64(openFiles), 1.0)
|
|
}
|
|
|
|
// Runtime memory metrics.
|
|
runtime.ReadMemStats(&m)
|
|
s.holder.Stats.Gauge("HeapAlloc", float64(m.HeapAlloc), 1.0)
|
|
s.holder.Stats.Gauge("HeapInuse", float64(m.HeapInuse), 1.0)
|
|
s.holder.Stats.Gauge("StackInuse", float64(m.StackInuse), 1.0)
|
|
s.holder.Stats.Gauge("Mallocs", float64(m.Mallocs), 1.0)
|
|
s.holder.Stats.Gauge("Frees", float64(m.Frees), 1.0)
|
|
}
|
|
}
|
|
|
|
// countOpenFiles on operating systems that support lsof.
|
|
func countOpenFiles() (int, error) {
|
|
switch runtime.GOOS {
|
|
case "darwin", "linux", "unix", "freebsd":
|
|
// -b option avoid kernel blocks
|
|
pid := os.Getpid()
|
|
out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("lsof -b -p %v", pid)).Output()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("calling lsof: %s", err)
|
|
}
|
|
// only count lines with our pid, avoiding warning messages from -b
|
|
lines := strings.Split(string(out), strconv.Itoa(pid))
|
|
return len(lines), nil
|
|
case "windows":
|
|
// TODO: count open file handles on windows
|
|
return 0, errors.New("countOpenFiles() on Windows is not supported")
|
|
default:
|
|
return 0, errors.New("countOpenFiles() on this OS is not supported")
|
|
}
|
|
}
|
|
|
|
func expandDirName(path string) (string, error) {
|
|
prefix := "~" + string(filepath.Separator)
|
|
if strings.HasPrefix(path, prefix) {
|
|
HomeDir := os.Getenv("HOME")
|
|
if HomeDir == "" {
|
|
return "", errors.New("data directory not specified and no home dir available")
|
|
}
|
|
return filepath.Join(HomeDir, strings.TrimPrefix(path, prefix)), nil
|
|
}
|
|
return path, nil
|
|
}
|