mirror of
https://github.com/hanzoai/graph.git
synced 2026-10-30 18:35:37 +00:00
Derived from Dgraph (github.com/dgraph-io/dgraph) at commit 64804bd2, Apache-2.0. See NOTICE. WHAT THIS IS. The graph engine as a Go library: posting lists, the DQL parser, the type and tokenizer system, the index and schema layers, and the wire types. 39,170 lines of the upstream's 166,337. THE STORE is github.com/luxfi/zapdb, a Badger fork that kept the package name, the subpackages (options, pb, y) and the API — Open, OpenManaged, DB, Options — so the 74 import sites are aliased rather than rewritten. THE CODEC is ZAP. zapdb serializes with ZAP, not protobuf: its pb package builds the ZAP types by default and the protobuf ones only under `-tags grpc`, a path that does not compile (table/builder.go calls pb.Marshal, which that tag removes). One call site in posting needed converting; every other type here is Hanzo's own protobuf and is untouched. NO TRANSPORT. An embedded engine dials nothing, so nothing here imports google.golang.org/grpc. Removed: 20 transport functions in x, the generated service stubs for Raft, Zero and Worker (1,657 lines), and the JWT helper — identity belongs to IAM. The core called none of them. NOT KEPT: worker, conn, raftwal, the cluster commands, the GraphQL layer and the test harnesses. `query`, the DQL execution engine, went with them because it depends on worker; parsing DQL is available here, executing it is not. The history of the upstream project lives at github.com/dgraph-io/dgraph. This repository begins at the derivation because three quarters of that history describes code this fork does not carry.
1337 lines
38 KiB
Go
1337 lines
38 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package x
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
builtinGzip "compress/gzip"
|
|
"context"
|
|
cryptorand "crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/golang/glog"
|
|
"github.com/pkg/errors"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"golang.org/x/term"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
badger "github.com/luxfi/zapdb"
|
|
bo "github.com/luxfi/zapdb/options"
|
|
badgerpb "github.com/luxfi/zapdb/pb"
|
|
"github.com/dgraph-io/dgo/v250"
|
|
"github.com/dgraph-io/ristretto/v2/z"
|
|
)
|
|
|
|
// Error constants representing different types of errors.
|
|
var (
|
|
// ErrNoJwt is returned when JWT is not present in the context.
|
|
ErrNoJwt = errors.New("no accessJwt available")
|
|
// ErrorInvalidLogin is returned when username or password is incorrect in login
|
|
ErrorInvalidLogin = errors.New("invalid username or password")
|
|
// ErrConflict is returned when commit couldn't succeed due to conflicts.
|
|
ErrConflict = errors.New("Transaction conflict")
|
|
// ErrHashMismatch is returned when the hash does not matches the startTs
|
|
ErrHashMismatch = errors.New("hash mismatch the claimed startTs|namespace")
|
|
)
|
|
|
|
const (
|
|
// Success is equivalent to the HTTP 200 error code.
|
|
Success = "Success"
|
|
// ErrorUnauthorized is equivalent to the HTTP 401 error code.
|
|
ErrorUnauthorized = "ErrorUnauthorized"
|
|
// ErrorInvalidMethod is equivalent to the HTTP 405 error code.
|
|
ErrorInvalidMethod = "ErrorInvalidMethod"
|
|
// ErrorInvalidRequest is equivalent to the HTTP 400 error code.
|
|
ErrorInvalidRequest = "ErrorInvalidRequest"
|
|
// Error is a general error code.
|
|
Error = "Error"
|
|
// ErrorNoData is an error returned when the requested data cannot be returned.
|
|
ErrorNoData = "ErrorNoData"
|
|
// ValidHostnameRegex is a regex that accepts our expected hostname format.
|
|
ValidHostnameRegex = `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}` +
|
|
`[a-zA-Z0-9_-]{0,62})*[._]?$`
|
|
// Star is equivalent to using * in a mutation.
|
|
// When changing this value also remember to change in in client/client.go:DeleteEdges.
|
|
Star = "_STAR_ALL"
|
|
|
|
// GrpcMaxSize is the maximum possible size for a gRPC message.
|
|
// Dgraph uses the maximum size for the most flexibility (2GB - equal
|
|
// to the max grpc frame size). Users will still need to set the max
|
|
// message sizes allowable on the client size when dialing.
|
|
GrpcMaxSize = math.MaxInt32
|
|
|
|
// PortZeroGrpc is the default gRPC port for zero.
|
|
PortZeroGrpc = 5080
|
|
// PortZeroHTTP is the default HTTP port for zero.
|
|
PortZeroHTTP = 6080
|
|
// PortInternal is the default port for internal use.
|
|
PortInternal = 7080
|
|
// PortHTTP is the default HTTP port for alpha.
|
|
PortHTTP = 8080
|
|
// PortGrpc is the default gRPC port for alpha.
|
|
PortGrpc = 9080
|
|
// ForceAbortDifference is the maximum allowed difference between
|
|
// AppliedUntil - TxnMarks.DoneUntil() before old transactions start getting aborted.
|
|
ForceAbortDifference = 5000
|
|
|
|
// FacetDelimiter is the symbol used to distinguish predicate names from facets.
|
|
FacetDelimiter = "|"
|
|
|
|
// GrootId is the ID of the admin user for ACLs.
|
|
GrootId = "groot"
|
|
// SuperAdminId is the ID of the admin group for ACLs.
|
|
SuperAdminId = "guardians"
|
|
|
|
// GroupIdFileName is the name of the file storing the ID of the group to which
|
|
// the data in a postings directory belongs. This ID is used to join the proper
|
|
// group the first time an Alpha comes up with data from a restored backup or a
|
|
// bulk load.
|
|
GroupIdFileName = "group_id"
|
|
|
|
// DefaultCreds is the default credentials for login via dgo client.
|
|
DefaultCreds = "user=; password=; namespace=0;"
|
|
|
|
AccessControlAllowedHeaders = "X-Dgraph-AccessToken, X-Dgraph-AuthToken, " +
|
|
"Content-Type, Content-Length, Accept-Encoding, Cache-Control, " +
|
|
"X-CSRF-Token, X-Auth-Token, X-Requested-With"
|
|
DgraphCostHeader = "Dgraph-TouchedUids"
|
|
|
|
ManifestVersion = 2105
|
|
)
|
|
|
|
var (
|
|
// Useful for running multiple servers on the same machine.
|
|
regExpHostName = regexp.MustCompile(ValidHostnameRegex)
|
|
// Nilbyte is a nil byte slice. Used
|
|
Nilbyte []byte
|
|
// SuperAdminUid is a map from namespace to the Uid of superadmin group node.
|
|
SuperAdminUid = &sync.Map{}
|
|
// GrootUid is a map from namespace to the Uid of groot user node.
|
|
GrootUid = &sync.Map{}
|
|
)
|
|
|
|
func init() {
|
|
SuperAdminUid.Store(RootNamespace, 0)
|
|
GrootUid.Store(RootNamespace, 0)
|
|
}
|
|
|
|
// SafeUTF8 sanitizes a string for OTLP export by replacing invalid UTF-8 sequences.
|
|
func SafeUTF8(s string) string {
|
|
return strings.ToValidUTF8(s, "?")
|
|
}
|
|
|
|
// ShouldCrash returns true if the error should cause the process to crash.
|
|
|
|
// WhiteSpace Replacer removes spaces and tabs from a string.
|
|
var WhiteSpace = strings.NewReplacer(" ", "", "\t", "")
|
|
|
|
// GqlError is a GraphQL spec compliant error structure. See GraphQL spec on
|
|
// errors here: https://graphql.github.io/graphql-spec/June2018/#sec-Errors
|
|
//
|
|
// Note: "Every error must contain an entry with the key message with a string
|
|
// description of the error intended for the developer as a guide to understand
|
|
// and correct the error."
|
|
//
|
|
// "If an error can be associated to a particular point in the request [the error]
|
|
// should contain an entry with the key locations with a list of locations"
|
|
//
|
|
// Path is about GraphQL results and Errors for GraphQL layer.
|
|
//
|
|
// Extensions is for everything else.
|
|
type GqlError struct {
|
|
Message string `json:"message"`
|
|
Locations []Location `json:"locations,omitempty"`
|
|
Path []interface{} `json:"path,omitempty"`
|
|
Extensions map[string]interface{} `json:"extensions,omitempty"`
|
|
}
|
|
|
|
// A Location is the Line+Column index of an error in a request.
|
|
type Location struct {
|
|
Line int `json:"line,omitempty"`
|
|
Column int `json:"column,omitempty"`
|
|
}
|
|
|
|
// GqlErrorList is a list of GraphQL errors as would be found in a response.
|
|
type GqlErrorList []*GqlError
|
|
|
|
type queryRes struct {
|
|
Errors GqlErrorList `json:"errors"`
|
|
}
|
|
|
|
// IsGqlErrorList tells whether the given err is a list of GraphQL errors.
|
|
func IsGqlErrorList(err error) bool {
|
|
_, ok := err.(GqlErrorList)
|
|
return ok
|
|
}
|
|
|
|
func (gqlErr *GqlError) Error() string {
|
|
var buf bytes.Buffer
|
|
if gqlErr == nil {
|
|
return ""
|
|
}
|
|
|
|
Check2(buf.WriteString(gqlErr.Message))
|
|
|
|
if len(gqlErr.Locations) > 0 {
|
|
Check2(buf.WriteString(" (Locations: ["))
|
|
for i, loc := range gqlErr.Locations {
|
|
if i > 0 {
|
|
Check2(buf.WriteString(", "))
|
|
}
|
|
Check2(buf.WriteString(fmt.Sprintf("{Line: %v, Column: %v}", loc.Line, loc.Column)))
|
|
}
|
|
Check2(buf.WriteString("])"))
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func (errList GqlErrorList) Error() string {
|
|
var buf bytes.Buffer
|
|
for i, gqlErr := range errList {
|
|
if i > 0 {
|
|
Check(buf.WriteByte('\n'))
|
|
}
|
|
Check2(buf.WriteString(gqlErr.Error()))
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// GqlErrorf returns a new GqlError with the message and args Sprintf'ed as the
|
|
// GqlError's Message.
|
|
func GqlErrorf(message string, args ...interface{}) *GqlError {
|
|
return &GqlError{
|
|
Message: fmt.Sprintf(message, args...),
|
|
}
|
|
}
|
|
|
|
// ExtractNamespaceHTTP parses the namespace value from the incoming HTTP request.
|
|
|
|
// ExtractNamespace parses the namespace value from the incoming gRPC context. For the non-ACL mode,
|
|
// it is caller's responsibility to set the galaxy namespace.
|
|
|
|
|
|
|
|
|
|
|
|
// WithLocations adds a list of locations to a GqlError and returns the same
|
|
// GqlError (fluent style).
|
|
func (gqlErr *GqlError) WithLocations(locs ...Location) *GqlError {
|
|
if gqlErr == nil {
|
|
return nil
|
|
}
|
|
|
|
gqlErr.Locations = append(gqlErr.Locations, locs...)
|
|
return gqlErr
|
|
}
|
|
|
|
// WithPath adds a path to a GqlError and returns the same
|
|
// GqlError (fluent style).
|
|
func (gqlErr *GqlError) WithPath(path []interface{}) *GqlError {
|
|
if gqlErr == nil {
|
|
return nil
|
|
}
|
|
|
|
gqlErr.Path = path
|
|
return gqlErr
|
|
}
|
|
|
|
// SetStatus sets the error code, message and the newly assigned uids
|
|
// in the http response.
|
|
func SetStatus(w http.ResponseWriter, code, msg string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var qr queryRes
|
|
ext := make(map[string]interface{})
|
|
ext["code"] = code
|
|
qr.Errors = append(qr.Errors, &GqlError{Message: msg, Extensions: ext})
|
|
if js, err := json.Marshal(qr); err == nil {
|
|
if _, err := w.Write(js); err != nil {
|
|
glog.Errorf("Could not send error msg=%+v code=%+v due to http error %+v", msg, code, err)
|
|
}
|
|
} else {
|
|
Panic(errors.Errorf("Unable to marshal: %+v", qr))
|
|
}
|
|
}
|
|
|
|
func SetStatusWithErrors(w http.ResponseWriter, code string, errs []string) {
|
|
var qr queryRes
|
|
ext := make(map[string]interface{})
|
|
ext["code"] = code
|
|
for _, err := range errs {
|
|
qr.Errors = append(qr.Errors, &GqlError{Message: err, Extensions: ext})
|
|
}
|
|
if js, err := json.Marshal(qr); err == nil {
|
|
if _, err := w.Write(js); err != nil {
|
|
glog.Errorf("Error while writing: %+v", err)
|
|
}
|
|
} else {
|
|
Panic(errors.Errorf("Unable to marshal: %+v", qr))
|
|
}
|
|
}
|
|
|
|
// SetHttpStatus is similar to SetStatus but sets a proper HTTP status code
|
|
// in the response instead of always returning HTTP 200 (OK).
|
|
func SetHttpStatus(w http.ResponseWriter, code int, msg string) {
|
|
w.WriteHeader(code)
|
|
SetStatus(w, "error", msg)
|
|
}
|
|
|
|
// AddCorsHeaders adds the CORS headers to an HTTP response.
|
|
func AddCorsHeaders(w http.ResponseWriter) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", AccessControlAllowedHeaders)
|
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
w.Header().Set("Connection", "close")
|
|
}
|
|
|
|
// QueryResWithData represents a response that holds errors as well as data.
|
|
type QueryResWithData struct {
|
|
Errors GqlErrorList `json:"errors"`
|
|
Data *string `json:"data"`
|
|
}
|
|
|
|
// SetStatusWithData sets the errors in the response and ensures that the data key
|
|
// in the data is present with value nil.
|
|
// In case an error was encountered after the query execution started, we have to return data
|
|
// key with null value according to GraphQL spec.
|
|
func SetStatusWithData(w http.ResponseWriter, code, msg string) {
|
|
var qr QueryResWithData
|
|
ext := make(map[string]interface{})
|
|
ext["code"] = code
|
|
qr.Errors = append(qr.Errors, &GqlError{Message: msg, Extensions: ext})
|
|
// This would ensure that data key is present with value null.
|
|
if js, err := json.Marshal(qr); err == nil {
|
|
if _, err := w.Write(js); err != nil {
|
|
glog.Errorf("Error while writing: %+v", err)
|
|
}
|
|
} else {
|
|
Panic(errors.Errorf("Unable to marshal: %+v", qr))
|
|
}
|
|
}
|
|
|
|
// Reply sets the body of an HTTP response to the JSON representation of the given reply.
|
|
func Reply(w http.ResponseWriter, rep interface{}) {
|
|
if js, err := json.Marshal(rep); err == nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, string(js))
|
|
} else {
|
|
SetStatus(w, Error, "Internal server error")
|
|
}
|
|
}
|
|
|
|
// ParseRequest parses the body of the given request.
|
|
func ParseRequest(w http.ResponseWriter, r *http.Request, data interface{}) bool {
|
|
defer func() {
|
|
if err := r.Body.Close(); err != nil {
|
|
glog.Warningf("error closing body: %v", err)
|
|
}
|
|
}()
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
if err := decoder.Decode(&data); err != nil {
|
|
SetStatus(w, Error, fmt.Sprintf("While parsing request: %v", err))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// AttachJWTNamespace attaches the namespace in the JWT claims to the context if present, otherwise
|
|
// it attaches the galaxy namespace.
|
|
|
|
// AttachNamespace adds given namespace to the metadata of the context.
|
|
|
|
// AttachJWTNamespaceOutgoing attaches the namespace in the JWT claims to the outgoing metadata of
|
|
// the context.
|
|
|
|
// AttachNamespaceOutgoing adds given namespace in the outgoing metadata of the context.
|
|
|
|
// AttachRootNsOperation specifies in the context that it will be used for doing a Root Namespace operation.
|
|
|
|
// AttachAuthToken adds any incoming PoorMan's auth header data into the grpc context metadata
|
|
|
|
// AttachAccessJwt adds any incoming JWT header data into the grpc context metadata
|
|
|
|
// AttachRemoteIP adds any incoming IP data into the grpc context metadata
|
|
|
|
// IsIpWhitelisted checks if the given ipString is a loopback address or within one of the
|
|
// configured whitelisted IP ranges. It is the exported entry point used by HTTP handlers that
|
|
// need to authorize a request by its source IP (gRPC handlers should use HasWhitelistedIP).
|
|
func IsIpWhitelisted(ipString string) bool {
|
|
return isIpWhitelisted(ipString)
|
|
}
|
|
|
|
// isIpWhitelisted checks if the given ipString is within the whitelisted ip range
|
|
func isIpWhitelisted(ipString string) bool {
|
|
ip := net.ParseIP(ipString)
|
|
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
if ip.IsLoopback() {
|
|
return true
|
|
}
|
|
|
|
for _, ipRange := range WorkerConfig.WhiteListedIPRanges {
|
|
if bytes.Compare(ip, ipRange.Lower) >= 0 && bytes.Compare(ip, ipRange.Upper) <= 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetIPsFromString parses a --security "whitelist=..." style value into a list of IP ranges.
|
|
// The value is a comma-separated list where each entry is a single IP or hostname, a
|
|
// "lower:upper" IP range, or a CIDR block. An empty string yields an empty (non-nil) list.
|
|
func GetIPsFromString(str string) ([]IPRange, error) {
|
|
if str == "" {
|
|
return []IPRange{}, nil
|
|
}
|
|
|
|
var ipRanges []IPRange
|
|
rangeStrings := strings.Split(str, ",")
|
|
|
|
for _, s := range rangeStrings {
|
|
isIPv6 := strings.Contains(s, "::")
|
|
tuple := strings.Split(s, ":")
|
|
switch {
|
|
case isIPv6 || len(tuple) == 1:
|
|
if !strings.Contains(s, "/") {
|
|
// string is hostname like host.docker.internal,
|
|
// or IPv4 address like 144.124.126.254,
|
|
// or IPv6 address like fd03:b188:0f3c:9ec4::babe:face
|
|
ipAddr := net.ParseIP(s)
|
|
if ipAddr != nil {
|
|
ipRanges = append(ipRanges, IPRange{Lower: ipAddr, Upper: ipAddr})
|
|
} else {
|
|
ipAddrs, err := net.LookupIP(s)
|
|
if err != nil {
|
|
return nil, errors.Errorf("invalid IP address or hostname: %s", s)
|
|
}
|
|
|
|
for _, addr := range ipAddrs {
|
|
ipRanges = append(ipRanges, IPRange{Lower: addr, Upper: addr})
|
|
}
|
|
}
|
|
} else {
|
|
// string is CIDR block like 192.168.0.0/16 or fd03:b188:0f3c:9ec4::/64
|
|
rangeLo, network, err := net.ParseCIDR(s)
|
|
if err != nil {
|
|
return nil, errors.Errorf("invalid CIDR block: %s", s)
|
|
}
|
|
|
|
addrLen, maskLen := len(rangeLo), len(network.Mask)
|
|
rangeHi := make(net.IP, len(rangeLo))
|
|
copy(rangeHi, rangeLo)
|
|
for i := 1; i <= maskLen; i++ {
|
|
rangeHi[addrLen-i] |= ^network.Mask[maskLen-i]
|
|
}
|
|
|
|
ipRanges = append(ipRanges, IPRange{Lower: rangeLo, Upper: rangeHi})
|
|
}
|
|
case len(tuple) == 2:
|
|
// string is range like a.b.c.d:w.x.y.z
|
|
rangeLo := net.ParseIP(tuple[0])
|
|
rangeHi := net.ParseIP(tuple[1])
|
|
switch {
|
|
case rangeLo == nil:
|
|
return nil, errors.Errorf("invalid IP address: %s", tuple[0])
|
|
case rangeHi == nil:
|
|
return nil, errors.Errorf("invalid IP address: %s", tuple[1])
|
|
case bytes.Compare(rangeLo, rangeHi) > 0:
|
|
return nil, errors.Errorf("inverted IP address range: %s", s)
|
|
}
|
|
ipRanges = append(ipRanges, IPRange{Lower: rangeLo, Upper: rangeHi})
|
|
default:
|
|
return nil, errors.Errorf("invalid IP address range: %s", s)
|
|
}
|
|
}
|
|
|
|
return ipRanges, nil
|
|
}
|
|
|
|
// HasWhitelistedIP checks whether the source IP in ctx is whitelisted or not.
|
|
// It returns the IP address if the IP is whitelisted, otherwise an error is returned.
|
|
|
|
// WriteResponse writes response body, transparently compressing if necessary.
|
|
func WriteResponse(w http.ResponseWriter, r *http.Request, b []byte) (int, error) {
|
|
var out io.Writer = w
|
|
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
gzw := builtinGzip.NewWriter(w)
|
|
defer gzw.Close()
|
|
out = gzw
|
|
}
|
|
|
|
bytesWritten, err := out.Write(b)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
w.Header().Set("Content-Length", strconv.FormatInt(int64(bytesWritten), 10))
|
|
return bytesWritten, nil
|
|
}
|
|
|
|
// Min returns the minimum of the two given numbers.
|
|
func Min(a, b uint64) uint64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Max returns the maximum of the two given numbers.
|
|
func Max(a, b uint64) uint64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// ExponentialRetry runs the given function until it succeeds or can no longer be retried.
|
|
func ExponentialRetry(maxRetries int, waitAfterFailure time.Duration,
|
|
f func() error) error {
|
|
var err error
|
|
for retry := maxRetries; retry > 0; retry-- {
|
|
if err = f(); err == nil {
|
|
return nil
|
|
}
|
|
if waitAfterFailure > 0 {
|
|
time.Sleep(waitAfterFailure)
|
|
waitAfterFailure *= 2
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RetryUntilSuccess runs the given function until it succeeds or can no longer be retried.
|
|
func RetryUntilSuccess(maxRetries int, waitAfterFailure time.Duration,
|
|
f func() error) error {
|
|
var err error
|
|
for retry := maxRetries; retry > 0; retry-- {
|
|
if err = f(); err == nil {
|
|
return nil
|
|
}
|
|
if waitAfterFailure > 0 {
|
|
time.Sleep(waitAfterFailure)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// {2 bytes Node ID} {4 bytes for random} {2 bytes zero}
|
|
func ProposalKey(id uint64) (uint64, error) {
|
|
random4Bytes := make([]byte, 4)
|
|
if _, err := cryptorand.Read(random4Bytes); err != nil {
|
|
return 0, err
|
|
}
|
|
proposalKey := id<<48 | uint64(binary.BigEndian.Uint32(random4Bytes))<<16
|
|
// We want to avoid spillage to node id in case of overflow. For instance, if the
|
|
// random bytes end up being [xx,xx, 255, 255, 255, 255, 0 , 0] (xx, xx being the node id)
|
|
// we would spill to node id after 65535 calls to unique key.
|
|
// So by setting 48th bit to 0 we ensure that we never spill out to node ids.
|
|
proposalKey &= ^(uint64(1) << 47)
|
|
return proposalKey, nil
|
|
}
|
|
|
|
// HasString returns whether the slice contains the given string.
|
|
func HasString(a []string, b string) bool {
|
|
for _, k := range a {
|
|
if k == b {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Unique takes an array and returns it with no duplicate entries.
|
|
func Unique(a []string) []string {
|
|
if len(a) < 2 {
|
|
return a
|
|
}
|
|
|
|
sort.Strings(a)
|
|
idx := 1
|
|
for _, val := range a {
|
|
if a[idx-1] == val {
|
|
continue
|
|
}
|
|
a[idx] = val
|
|
idx++
|
|
}
|
|
return a[:idx]
|
|
}
|
|
|
|
// ReadLine reads a single line from a buffered reader. The line is read into the
|
|
// passed in buffer to minimize allocations. This is the preferred
|
|
// method for loading long lines which could be longer than the buffer
|
|
// size of bufio.Scanner.
|
|
func ReadLine(r *bufio.Reader, buf *bytes.Buffer) error {
|
|
isPrefix := true
|
|
var err error
|
|
buf.Reset()
|
|
for isPrefix && err == nil {
|
|
var line []byte
|
|
// The returned line is an pb.buffer in bufio and is only
|
|
// valid until the next call to ReadLine. It needs to be copied
|
|
// over to our own buffer.
|
|
line, isPrefix, err = r.ReadLine()
|
|
if err == nil {
|
|
if _, err := buf.Write(line); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// FixedDuration returns the given duration as a string of fixed length.
|
|
func FixedDuration(d time.Duration) string {
|
|
str := fmt.Sprintf("%02ds", int(d.Seconds())%60)
|
|
if d >= time.Minute {
|
|
str = fmt.Sprintf("%02dm", int(d.Minutes())%60) + str
|
|
}
|
|
if d >= time.Hour {
|
|
str = fmt.Sprintf("%02dh", int(d.Hours())) + str
|
|
}
|
|
return str
|
|
}
|
|
|
|
// PageRange returns start and end indices given pagination params. Note that n
|
|
// is the size of the input list.
|
|
func PageRange(count, offset, n int) (int, int) {
|
|
if n == 0 {
|
|
return 0, 0
|
|
}
|
|
if count == 0 && offset == 0 {
|
|
return 0, n
|
|
}
|
|
if count < 0 {
|
|
// Items from the back of the array, like Python arrays. Do a positive mod n.
|
|
if count*-1 > n {
|
|
count = -n
|
|
}
|
|
return (((n + count) % n) + n) % n, n
|
|
}
|
|
start := offset
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
if start > n {
|
|
return n, n
|
|
}
|
|
if count == 0 { // No count specified. Just take the offset parameter.
|
|
return start, n
|
|
}
|
|
end := start + count
|
|
if end > n {
|
|
end = n
|
|
}
|
|
return start, end
|
|
}
|
|
|
|
// ValidateAddress checks whether given address can be used with grpc dial function
|
|
func ValidateAddress(addr string) error {
|
|
host, port, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p, err := strconv.Atoi(port); err != nil || p <= 0 || p >= 65536 {
|
|
return errors.Errorf("Invalid port: %v", p)
|
|
}
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return nil
|
|
}
|
|
// try to parse as hostname as per hostname RFC
|
|
if len(strings.Replace(host, ".", "", -1)) > 255 {
|
|
return errors.Errorf("Hostname should be less than or equal to 255 characters")
|
|
}
|
|
if !regExpHostName.MatchString(host) {
|
|
return errors.Errorf("Invalid hostname: %v", host)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveDuplicates sorts the slice of strings and removes duplicates. changes the input slice.
|
|
// This function should be called like: someSlice = RemoveDuplicates(someSlice)
|
|
func RemoveDuplicates(s []string) (out []string) {
|
|
sort.Strings(s)
|
|
out = s[:0]
|
|
for i := range s {
|
|
if i > 0 && s[i] == s[i-1] {
|
|
continue
|
|
}
|
|
out = append(out, s[i])
|
|
}
|
|
return
|
|
}
|
|
|
|
// BytesBuffer provides a buffer backed by byte slices.
|
|
type BytesBuffer struct {
|
|
data [][]byte
|
|
off int
|
|
sz int
|
|
}
|
|
|
|
func (b *BytesBuffer) grow(n int) {
|
|
if n < 128 {
|
|
n = 128
|
|
}
|
|
if len(b.data) == 0 {
|
|
b.data = append(b.data, make([]byte, n))
|
|
}
|
|
|
|
last := len(b.data) - 1
|
|
// Return if we have sufficient space
|
|
if len(b.data[last])-b.off >= n {
|
|
return
|
|
}
|
|
sz := len(b.data[last]) * 2
|
|
if sz > 512<<10 {
|
|
sz = 512 << 10 // 512 KB
|
|
}
|
|
if sz < n {
|
|
sz = n
|
|
}
|
|
b.data[last] = b.data[last][:b.off]
|
|
b.sz += len(b.data[last])
|
|
b.data = append(b.data, make([]byte, sz))
|
|
b.off = 0
|
|
}
|
|
|
|
// Slice returns a slice of length n to be used for writing.
|
|
func (b *BytesBuffer) Slice(n int) []byte {
|
|
b.grow(n)
|
|
last := len(b.data) - 1
|
|
b.off += n
|
|
b.sz += n
|
|
return b.data[last][b.off-n : b.off]
|
|
}
|
|
|
|
// Length returns the size of the buffer.
|
|
func (b *BytesBuffer) Length() int {
|
|
return b.sz
|
|
}
|
|
|
|
// CopyTo copies the contents of the buffer to the given byte slice.
|
|
// Caller should ensure that o is of appropriate length.
|
|
func (b *BytesBuffer) CopyTo(o []byte) int {
|
|
offset := 0
|
|
for i, d := range b.data {
|
|
if i == len(b.data)-1 {
|
|
copy(o[offset:], d[:b.off])
|
|
offset += b.off
|
|
} else {
|
|
copy(o[offset:], d)
|
|
offset += len(d)
|
|
}
|
|
}
|
|
return offset
|
|
}
|
|
|
|
// TruncateBy reduces the size of the bugger by the given amount.
|
|
// Always give back <= touched bytes.
|
|
func (b *BytesBuffer) TruncateBy(n int) {
|
|
b.off -= n
|
|
b.sz -= n
|
|
AssertTrue(b.off >= 0 && b.sz >= 0)
|
|
}
|
|
|
|
type record struct {
|
|
Name string
|
|
Dur time.Duration
|
|
}
|
|
|
|
// Timer implements a timer that supports recording the duration of events.
|
|
type Timer struct {
|
|
start time.Time
|
|
last time.Time
|
|
records []record
|
|
}
|
|
|
|
// Start starts the timer and clears the list of records.
|
|
func (t *Timer) Start() {
|
|
t.start = time.Now()
|
|
t.last = t.start
|
|
t.records = t.records[:0]
|
|
}
|
|
|
|
// Record records an event and assigns it the given name.
|
|
func (t *Timer) Record(name string) {
|
|
now := time.Now()
|
|
t.records = append(t.records, record{
|
|
Name: name,
|
|
Dur: now.Sub(t.last).Round(time.Millisecond),
|
|
})
|
|
t.last = now
|
|
}
|
|
|
|
// Total returns the duration since the timer was started.
|
|
func (t *Timer) Total() time.Duration {
|
|
return time.Since(t.start).Round(time.Millisecond)
|
|
}
|
|
|
|
func (t *Timer) String() string {
|
|
sort.Slice(t.records, func(i, j int) bool {
|
|
return t.records[i].Dur > t.records[j].Dur
|
|
})
|
|
return fmt.Sprintf("Timer Total: %s. Breakdown: %v", t.Total(), t.records)
|
|
}
|
|
|
|
// PredicateLang extracts the language from a predicate (or facet) name.
|
|
// Returns the predicate and the language tag, if any.
|
|
func PredicateLang(s string) (string, string) {
|
|
i := strings.LastIndex(s, "@")
|
|
if i <= 0 {
|
|
return s, ""
|
|
}
|
|
return s[0:i], s[i+1:]
|
|
}
|
|
|
|
// DivideAndRule is used to divide a number of tasks among multiple go routines.
|
|
func DivideAndRule(num int) (numGo, width int) {
|
|
numGo, width = 64, 0
|
|
for ; numGo >= 1; numGo /= 2 {
|
|
widthF := math.Ceil(float64(num) / float64(numGo))
|
|
if numGo == 1 || widthF >= 256.0 {
|
|
width = int(widthF)
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// SetupConnection starts a secure gRPC connection to the given host.
|
|
|
|
// Diff computes the difference between the keys of the two given maps.
|
|
func Diff(dst map[string]struct{}, src map[string]struct{}) ([]string, []string) {
|
|
var add []string
|
|
var del []string
|
|
|
|
for g := range dst {
|
|
if _, ok := src[g]; !ok {
|
|
add = append(add, g)
|
|
}
|
|
}
|
|
for g := range src {
|
|
if _, ok := dst[g]; !ok {
|
|
del = append(del, g)
|
|
}
|
|
}
|
|
|
|
return add, del
|
|
}
|
|
|
|
// SpanTimer returns a function used to record the duration of the given span.
|
|
// It supports both OpenCensus and OpenTelemetry spans
|
|
func SpanTimer(spanInterface interface{}, name string) func() {
|
|
// OpenCensus span case
|
|
if span, ok := spanInterface.(*trace.Span); ok && span != nil {
|
|
uniq := int64(rand.Int31()) //nolint:gosec // unique id for tracing does not require cryptographic precision
|
|
attrs := trace.WithAttributes(
|
|
attribute.Int64("funcId", uniq),
|
|
attribute.String("funcName", name),
|
|
)
|
|
(*span).AddEvent("Start", attrs)
|
|
start := time.Now()
|
|
|
|
return func() {
|
|
(*span).AddEvent("End", trace.WithAttributes(
|
|
attribute.Int64("funcId", uniq),
|
|
attribute.String("funcName", name),
|
|
attribute.String("duration", time.Since(start).String()),
|
|
))
|
|
// TODO: We can look into doing a latency record here.
|
|
}
|
|
}
|
|
|
|
// OpenTelemetry span case
|
|
if span, ok := spanInterface.(trace.Span); ok {
|
|
uniq := int64(rand.Int31()) //nolint:gosec // unique id for tracing does not require cryptographic precision
|
|
attrs := trace.WithAttributes(
|
|
attribute.Int64("funcId", uniq),
|
|
attribute.String("funcName", name),
|
|
)
|
|
span.AddEvent("Start", attrs)
|
|
start := time.Now()
|
|
|
|
return func() {
|
|
span.AddEvent("End", trace.WithAttributes(
|
|
attribute.Int64("funcId", uniq),
|
|
attribute.String("funcName", name),
|
|
attribute.String("duration", time.Since(start).String()),
|
|
))
|
|
// TODO: We can look into doing a latency record here.
|
|
}
|
|
}
|
|
|
|
// Fall back for nil or unknown span type
|
|
return func() {}
|
|
}
|
|
|
|
// CloseFunc needs to be called to close all the client connections.
|
|
type CloseFunc func()
|
|
|
|
// CredOpt stores the options for logging in, including the password and user.
|
|
type CredOpt struct {
|
|
UserID string
|
|
Password string
|
|
Namespace uint64
|
|
}
|
|
|
|
type authorizationCredentials struct {
|
|
token string
|
|
}
|
|
|
|
func (a *authorizationCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
|
return map[string]string{"Authorization": a.token}, nil
|
|
}
|
|
|
|
func (a *authorizationCredentials) RequireTransportSecurity() bool {
|
|
return true
|
|
}
|
|
|
|
// WithAuthorizationCredentials adds Authorization: <api-token> to every GRPC request
|
|
// This is mostly used by Slash GraphQL to authenticate requests
|
|
|
|
// GetDgraphClient creates a Dgraph client based on the following options in the configuration:
|
|
// --slash_grpc_endpoint specifies the grpc endpoint for slash. It takes precedence over --alpha and TLS
|
|
// --alpha specifies a comma separated list of endpoints to connect to
|
|
// --tls "ca-cert=; client-cert=; client-key=;" etc specify the TLS configuration of the connection
|
|
// --retries specifies how many times we should retry the connection to each endpoint upon failures
|
|
// --user and --password specify the credentials we should use to login with the server
|
|
|
|
// AskUserPassword prompts the user to enter the password for the given user ID.
|
|
func AskUserPassword(userid string, pwdType string, times int) (string, error) {
|
|
AssertTrue(times == 1 || times == 2)
|
|
AssertTrue(pwdType == "Current" || pwdType == "New")
|
|
// ask for the user's password
|
|
fmt.Printf("%s password for %v:", pwdType, userid)
|
|
pd, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "while reading password")
|
|
}
|
|
fmt.Println()
|
|
password := string(pd)
|
|
|
|
if times == 2 {
|
|
fmt.Printf("Retype %s password for %v:", strings.ToLower(pwdType), userid)
|
|
pd2, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "while reading password")
|
|
}
|
|
fmt.Println()
|
|
|
|
password2 := string(pd2)
|
|
if password2 != password {
|
|
return "", errors.Errorf("the two typed passwords do not match")
|
|
}
|
|
}
|
|
return password, nil
|
|
}
|
|
|
|
// GetPassAndLogin uses the given credentials and client to perform the login operation.
|
|
func GetPassAndLogin(dg *dgo.Dgraph, opt *CredOpt) error {
|
|
password := opt.Password
|
|
if len(password) == 0 {
|
|
var err error
|
|
password, err = AskUserPassword(opt.UserID, "Current", 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := dg.LoginIntoNamespace(ctx, opt.UserID, password, opt.Namespace); err != nil {
|
|
return errors.Wrapf(err, "unable to login to the %v account", opt.UserID)
|
|
}
|
|
fmt.Println("Login successful.")
|
|
// update the context so that it has the admin jwt token
|
|
return nil
|
|
}
|
|
|
|
func IsSuperAdmin(groups []string) bool {
|
|
for _, group := range groups {
|
|
if group == SuperAdminId {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// RunVlogGC runs value log gc on store. It runs GC unconditionally after every 1 minute.
|
|
func RunVlogGC(store *badger.DB, closer *z.Closer) {
|
|
defer closer.Done()
|
|
|
|
ticker := time.Tick(1 * time.Minute)
|
|
|
|
abs := func(a, b int64) int64 {
|
|
if a > b {
|
|
return a - b
|
|
}
|
|
return b - a
|
|
}
|
|
|
|
var lastSz int64
|
|
runGC := func() {
|
|
for err := error(nil); err == nil; {
|
|
// If a GC is successful, immediately run it again.
|
|
err = store.RunValueLogGC(0.7)
|
|
}
|
|
_, sz := store.Size()
|
|
if abs(lastSz, sz) > 512<<20 {
|
|
glog.V(2).Infof("Value log size: %s\n", humanize.IBytes(uint64(sz)))
|
|
lastSz = sz
|
|
}
|
|
}
|
|
|
|
runGC()
|
|
for {
|
|
select {
|
|
case <-closer.HasBeenClosed():
|
|
return
|
|
case <-ticker:
|
|
runGC()
|
|
}
|
|
}
|
|
}
|
|
|
|
type DB interface {
|
|
Sync() error
|
|
}
|
|
|
|
func StoreSync(db DB, closer *z.Closer) {
|
|
defer closer.Done()
|
|
// We technically don't need to call this due to mmap being able to survive process crashes.
|
|
// But, once a minute is infrequent enough that we won't lose any performance due to this.
|
|
ticker := time.Tick(time.Minute)
|
|
|
|
for {
|
|
select {
|
|
case <-ticker:
|
|
if err := db.Sync(); err != nil {
|
|
glog.Errorf("Error while calling db sync: %+v", err)
|
|
}
|
|
case <-closer.HasBeenClosed():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// DeepCopyJsonMap returns a deep copy of the input map `m`.
|
|
// `m` is supposed to be a map similar to the ones produced as a result of json unmarshalling. i.e.,
|
|
// any value in `m` at any nested level should be of an inbuilt go type.
|
|
func DeepCopyJsonMap(m map[string]interface{}) map[string]interface{} {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
mCopy := make(map[string]interface{})
|
|
for k, v := range m {
|
|
switch val := v.(type) {
|
|
case map[string]interface{}:
|
|
mCopy[k] = DeepCopyJsonMap(val)
|
|
case []interface{}:
|
|
mCopy[k] = DeepCopyJsonArray(val)
|
|
default:
|
|
mCopy[k] = val
|
|
}
|
|
}
|
|
return mCopy
|
|
}
|
|
|
|
// DeepCopyJsonArray returns a deep copy of the input array `a`.
|
|
// `a` is supposed to be an array similar to the ones produced as a result of json unmarshalling.
|
|
// i.e., any value in `a` at any nested level should be of an inbuilt go type.
|
|
func DeepCopyJsonArray(a []interface{}) []interface{} {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
|
|
aCopy := make([]interface{}, 0, len(a))
|
|
for _, v := range a {
|
|
switch val := v.(type) {
|
|
case map[string]interface{}:
|
|
aCopy = append(aCopy, DeepCopyJsonMap(val))
|
|
case []interface{}:
|
|
aCopy = append(aCopy, DeepCopyJsonArray(val))
|
|
default:
|
|
aCopy = append(aCopy, val)
|
|
}
|
|
}
|
|
return aCopy
|
|
}
|
|
|
|
// GetCachePercentages returns the slice of cache percentages given the "," (comma) separated
|
|
// cache percentages(integers) string and expected number of caches.
|
|
func GetCachePercentages(cpString string, numExpected int) ([]int64, error) {
|
|
cp := strings.Split(cpString, ",")
|
|
// Sanity checks
|
|
if len(cp) != numExpected {
|
|
return nil, errors.Errorf("ERROR: expected %d cache percentages, got %d",
|
|
numExpected, len(cp))
|
|
}
|
|
|
|
var cachePercent []int64
|
|
percentSum := 0
|
|
for _, percent := range cp {
|
|
x, err := strconv.Atoi(percent)
|
|
if err != nil {
|
|
return nil, errors.Errorf("ERROR: unable to parse cache percentage(%s)", percent)
|
|
}
|
|
if x < 0 {
|
|
return nil, errors.Errorf("ERROR: cache percentage(%s) cannot be negative", percent)
|
|
}
|
|
cachePercent = append(cachePercent, int64(x))
|
|
percentSum += x
|
|
}
|
|
|
|
if percentSum != 100 {
|
|
return nil, errors.Errorf("ERROR: cache percentages (%s) does not sum up to 100",
|
|
strings.Join(cp, "+"))
|
|
}
|
|
|
|
return cachePercent, nil
|
|
}
|
|
|
|
// ParseCompression returns badger.compressionType and compression level given compression string
|
|
// of format compression-type:compression-level
|
|
func ParseCompression(cStr string) (bo.CompressionType, int) {
|
|
cStrSplit := strings.Split(cStr, ":")
|
|
cType := cStrSplit[0]
|
|
level := 3
|
|
|
|
var err error
|
|
if len(cStrSplit) == 2 {
|
|
level, err = strconv.Atoi(cStrSplit[1])
|
|
Check(err)
|
|
if level <= 0 {
|
|
glog.Fatalf("ERROR: compression level(%v) must be greater than zero", level)
|
|
}
|
|
} else if len(cStrSplit) > 2 {
|
|
glog.Fatalf("ERROR: Invalid badger.compression argument")
|
|
}
|
|
switch cType {
|
|
case "zstd":
|
|
return bo.ZSTD, level
|
|
case "snappy":
|
|
return bo.Snappy, 0
|
|
case "none":
|
|
return bo.None, 0
|
|
}
|
|
glog.Fatalf("ERROR: compression type (%s) invalid", cType)
|
|
return 0, 0
|
|
}
|
|
|
|
// ToHex converts a uint64 to a hex byte array. If rdf is true it will
|
|
// use < > brackets to delimit the value. Otherwise it will use quotes
|
|
// like JSON requires.
|
|
func ToHex(i uint64, rdf bool) []byte {
|
|
var b [16]byte
|
|
tmp := strconv.AppendUint(b[:0], i, 16)
|
|
|
|
out := make([]byte, len(tmp)+3+1)
|
|
if rdf {
|
|
out[0] = '<'
|
|
} else {
|
|
out[0] = '"'
|
|
}
|
|
|
|
out[1] = '0'
|
|
out[2] = 'x'
|
|
n := copy(out[3:], tmp)
|
|
|
|
if rdf {
|
|
out[3+n] = '>'
|
|
} else {
|
|
out[3+n] = '"'
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func MarshalToSizedBuffer(buf []byte, m proto.Message) ([]byte, error) {
|
|
buf = buf[:0]
|
|
return proto.MarshalOptions{}.MarshalAppend(buf, m)
|
|
}
|
|
|
|
// RootTemplate defines the help template for dgraph command.
|
|
var RootTemplate string = `Dgraph is a horizontally scalable and distributed graph database,
|
|
providing ACID transactions, consistent replication and linearizable reads.
|
|
It's built from the ground up to perform for a rich set of queries. Being a native
|
|
graph database, it tightly controls how the data is arranged on disk to optimize
|
|
for query performance and throughput, reducing disk seeks and network calls in a
|
|
cluster.` + BuildDetails() +
|
|
`Usage:{{if .Runnable}}
|
|
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
|
{{.CommandPath}} [command]{{end}} {{if .HasAvailableSubCommands}}
|
|
|
|
Generic: {{range .Commands}} {{if (or (and .IsAvailableCommand (eq .Annotations.group "default")) (eq .Name "help"))}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
|
|
Available Commands:
|
|
|
|
Dgraph Core: {{range .Commands}} {{if (and .IsAvailableCommand (eq .Annotations.group "core"))}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
|
|
Data Loading: {{range .Commands}} {{if (and .IsAvailableCommand (eq .Annotations.group "data-load"))}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
|
|
Dgraph Security: {{range .Commands}} {{if (and .IsAvailableCommand (eq .Annotations.group "security"))}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
|
|
Dgraph Debug: {{range .Commands}} {{if (and .IsAvailableCommand (eq .Annotations.group "debug"))}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
|
|
Dgraph Tools: {{range .Commands}} {{if (and .IsAvailableCommand (eq .Annotations.group "tool"))}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
` +
|
|
// uncomment this part when new availalble commands are added
|
|
|
|
/*Additional Commands:{{range .Commands}}{{if (and .IsAvailableCommand (not .Annotations.group))}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}*/
|
|
`
|
|
Flags:
|
|
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
|
|
|
|
|
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
|
|
`
|
|
|
|
// NonRootTemplate defines the help template for dgraph sub-command.
|
|
var NonRootTemplate string = `{{if .Long}} {{.Long}} {{else}} {{.Short}} {{end}}
|
|
Usage:{{if .Runnable}}
|
|
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
|
{{.CommandPath}} [command]{{end}} {{if .HasAvailableSubCommands}}
|
|
|
|
Available Commands: {{range .Commands}}{{if (or .IsAvailableCommand)}}
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
|
|
Flags:
|
|
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
|
|
|
Global Flags:
|
|
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
|
|
|
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
|
|
`
|
|
|
|
// KvWithMaxVersion returns a KV with the max version from the list of KVs.
|
|
func KvWithMaxVersion(kvs *badgerpb.KVList, prefixes [][]byte) *badgerpb.KV {
|
|
// Iterate over kvs to get the KV with the latest version. It is not necessary that the last
|
|
// KV contain the latest value.
|
|
var maxKv *badgerpb.KV
|
|
for _, kv := range kvs.GetKv() {
|
|
if maxKv.GetVersion() <= kv.GetVersion() {
|
|
maxKv = kv
|
|
}
|
|
}
|
|
return maxKv
|
|
}
|
|
|
|
// PrefixesToMatches converts the prefixes for subscription to a list of match.
|
|
func PrefixesToMatches(prefixes [][]byte, ignore string) []*badgerpb.Match {
|
|
matches := make([]*badgerpb.Match, 0, len(prefixes))
|
|
for _, prefix := range prefixes {
|
|
matches = append(matches, &badgerpb.Match{
|
|
Prefix: prefix,
|
|
IgnoreBytes: ignore,
|
|
})
|
|
}
|
|
return matches
|
|
}
|
|
|
|
// LimiterConf is the configuration options for LimiterConf.
|
|
type LimiterConf struct {
|
|
UidLeaseLimit uint64
|
|
RefillAfter time.Duration
|
|
}
|
|
|
|
// RateLimiter implements a basic rate limiter.
|
|
type RateLimiter struct {
|
|
limiter *sync.Map
|
|
maxTokens int64
|
|
refillAfter time.Duration
|
|
closer *z.Closer
|
|
}
|
|
|
|
// NewRateLimiter creates a rate limiter that limits lease by maxTokens in an interval specified by
|
|
// refillAfter.
|
|
func NewRateLimiter(maxTokens int64, refillAfter time.Duration, closer *z.Closer) *RateLimiter {
|
|
r := &RateLimiter{
|
|
limiter: &sync.Map{},
|
|
maxTokens: maxTokens,
|
|
refillAfter: refillAfter,
|
|
closer: closer,
|
|
}
|
|
r.closer.AddRunning(1)
|
|
go r.RefillPeriodically()
|
|
return r
|
|
}
|
|
|
|
// Allow checks if the request for req number of tokens can be allowed for a given namespace.
|
|
// If request is allowed, it subtracts the req from the available tokens.
|
|
func (r *RateLimiter) Allow(ns uint64, req int64) bool {
|
|
v := r.maxTokens
|
|
val, _ := r.limiter.LoadOrStore(ns, &v)
|
|
ptr := val.(*int64)
|
|
if cnt := atomic.AddInt64(ptr, -req); cnt < 0 {
|
|
atomic.AddInt64(ptr, req)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RefillPeriodically refills the tokens of all the namespaces to maxTokens periodically .
|
|
func (r *RateLimiter) RefillPeriodically() {
|
|
defer r.closer.Done()
|
|
refill := func() {
|
|
r.limiter.Range(func(_, val interface{}) bool {
|
|
atomic.StoreInt64(val.(*int64), r.maxTokens)
|
|
return true
|
|
})
|
|
}
|
|
|
|
ticker := time.Tick(r.refillAfter)
|
|
|
|
for {
|
|
select {
|
|
case <-r.closer.HasBeenClosed():
|
|
return
|
|
case <-ticker:
|
|
refill()
|
|
}
|
|
}
|
|
}
|
|
|
|
// LambdaUrl returns the correct lambda-url for the given namespace
|
|
func LambdaUrl(ns uint64) string {
|
|
return strings.Replace(Config.GraphQL.GetString("lambda-url"), "$ns", strconv.FormatUint(ns,
|
|
10), 1)
|
|
}
|
|
|
|
// IsJwtExpired returns true if the error indicates that the jwt has expired.
|