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.
441 lines
11 KiB
Go
441 lines
11 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package posting
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
ostats "go.opencensus.io/stats"
|
|
"go.opencensus.io/tag"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
badger "github.com/luxfi/zapdb"
|
|
"github.com/dgraph-io/dgo/v250/protos/api"
|
|
"github.com/hanzoai/graph/protos/pb"
|
|
"github.com/hanzoai/graph/x"
|
|
"github.com/dgraph-io/ristretto/v2/z"
|
|
)
|
|
|
|
const (
|
|
mb = 1 << 20
|
|
)
|
|
|
|
var (
|
|
pstore *badger.DB
|
|
closer *z.Closer
|
|
EnableDetailedMetrics bool
|
|
)
|
|
|
|
// Init initializes the posting lists package, the in memory and dirty list hash.
|
|
func Init(ps *badger.DB, cacheSize int64, removeOnUpdate bool) {
|
|
pstore = ps
|
|
closer = z.NewCloser(1)
|
|
go x.MonitorMemoryMetrics(closer)
|
|
|
|
MemLayerInstance = initMemoryLayer(cacheSize, removeOnUpdate)
|
|
}
|
|
|
|
func SetEnabledDetailedMetrics(enableMetrics bool) {
|
|
EnableDetailedMetrics = enableMetrics
|
|
}
|
|
|
|
// Cleanup waits until the closer has finished processing.
|
|
func Cleanup() {
|
|
closer.SignalAndWait()
|
|
}
|
|
|
|
// GetNoStore returns the list stored in the key or creates a new one if it doesn't exist.
|
|
// It does not store the list in any cache.
|
|
func GetNoStore(key []byte, readTs uint64) (rlist *List, err error) {
|
|
return getNew(key, pstore, readTs, false)
|
|
}
|
|
|
|
// LocalCache stores a cache of posting lists and deltas.
|
|
// This doesn't sync, so call this only when you don't care about dirty posting lists in
|
|
// memory(for example before populating snapshot) or after calling syncAllMarks
|
|
type LocalCache struct {
|
|
sync.RWMutex
|
|
|
|
startTs uint64
|
|
commitTs uint64
|
|
|
|
// The keys for these maps is a string representation of the Badger key for the posting list.
|
|
// deltas keep track of the updates made by txn. These must be kept around until written to disk
|
|
// during commit.
|
|
deltas map[string][]byte
|
|
|
|
// max committed timestamp of the read posting lists.
|
|
maxVersions map[string]uint64
|
|
|
|
// plists are posting lists in memory. They can be discarded to reclaim space.
|
|
plists map[string]*List
|
|
}
|
|
|
|
// struct to implement LocalCache interface from vector-indexer
|
|
// acts as wrapper for dgraph *LocalCache
|
|
type viLocalCache struct {
|
|
delegate *LocalCache
|
|
}
|
|
|
|
func (vc *viLocalCache) Find(prefix []byte, filter func([]byte) bool) (uint64, error) {
|
|
return vc.delegate.Find(prefix, filter)
|
|
}
|
|
|
|
func (vc *viLocalCache) Get(key []byte) ([]byte, error) {
|
|
pl, err := vc.delegate.Get(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pl.Lock()
|
|
defer pl.Unlock()
|
|
return vc.GetValueFromPostingList(pl)
|
|
}
|
|
|
|
func (vc *viLocalCache) GetWithLockHeld(key []byte) ([]byte, error) {
|
|
pl, err := vc.delegate.Get(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return vc.GetValueFromPostingList(pl)
|
|
}
|
|
|
|
func (vc *viLocalCache) GetValueFromPostingList(pl *List) ([]byte, error) {
|
|
if pl.cache != nil {
|
|
return pl.cache, nil
|
|
}
|
|
value := pl.findStaticValue(vc.delegate.startTs)
|
|
|
|
if value == nil || len(value.Postings) == 0 {
|
|
return nil, ErrNoValue
|
|
}
|
|
|
|
if value.Postings[0].Op == Del {
|
|
return nil, ErrNoValue
|
|
}
|
|
|
|
pl.cache = value.Postings[0].Value
|
|
return pl.cache, nil
|
|
}
|
|
|
|
func NewViLocalCache(delegate *LocalCache) *viLocalCache {
|
|
return &viLocalCache{delegate: delegate}
|
|
}
|
|
|
|
// NewLocalCache returns a new LocalCache instance.
|
|
func NewLocalCache(startTs uint64) *LocalCache {
|
|
return &LocalCache{
|
|
startTs: startTs,
|
|
deltas: make(map[string][]byte),
|
|
plists: make(map[string]*List),
|
|
maxVersions: make(map[string]uint64),
|
|
}
|
|
}
|
|
|
|
// NoCache returns a new LocalCache instance, which won't cache anything. Useful to pass startTs
|
|
// around.
|
|
func NoCache(startTs uint64) *LocalCache {
|
|
return &LocalCache{startTs: startTs}
|
|
}
|
|
|
|
func (lc *LocalCache) UpdateCommitTs(commitTs uint64) {
|
|
lc.Lock()
|
|
defer lc.Unlock()
|
|
lc.commitTs = commitTs
|
|
}
|
|
|
|
func (lc *LocalCache) Find(pred []byte, filter func([]byte) bool) (uint64, error) {
|
|
txn := pstore.NewTransactionAt(lc.startTs, false)
|
|
defer txn.Discard()
|
|
|
|
attr := string(pred)
|
|
|
|
initKey := x.ParsedKey{
|
|
Attr: attr,
|
|
}
|
|
startKey := x.DataKey(attr, 0)
|
|
prefix := initKey.DataPrefix()
|
|
|
|
result := &pb.List{}
|
|
var prevKey []byte
|
|
itOpt := badger.DefaultIteratorOptions
|
|
itOpt.PrefetchValues = false
|
|
itOpt.AllVersions = true
|
|
itOpt.Prefix = prefix
|
|
it := txn.NewIterator(itOpt)
|
|
defer it.Close()
|
|
|
|
for it.Seek(startKey); it.Valid(); {
|
|
item := it.Item()
|
|
if bytes.Equal(item.Key(), prevKey) {
|
|
it.Next()
|
|
continue
|
|
}
|
|
prevKey = append(prevKey[:0], item.Key()...)
|
|
|
|
// Parse the key upfront, otherwise ReadPostingList would advance the
|
|
// iterator.
|
|
pk, err := x.Parse(item.Key())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// If we have moved to the next attribute, break
|
|
if pk.Attr != attr {
|
|
break
|
|
}
|
|
|
|
if pk.HasStartUid {
|
|
// The keys holding parts of a split key should not be accessed here because
|
|
// they have a different prefix. However, the check is being added to guard
|
|
// against future bugs.
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case item.UserMeta()&BitEmptyPosting > 0:
|
|
// This is an empty posting list. So, it should not be included.
|
|
continue
|
|
default:
|
|
// This bit would only be set if there are valid uids in UidPack.
|
|
key := x.DataKey(attr, pk.Uid)
|
|
pl, err := lc.Get(key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
vals, err := pl.Value(lc.startTs)
|
|
switch {
|
|
case err == ErrNoValue:
|
|
continue
|
|
case err != nil:
|
|
return 0, err
|
|
}
|
|
|
|
if filter(vals.Value.([]byte)) {
|
|
return pk.Uid, nil
|
|
}
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
if len(result.Uids) > 0 {
|
|
return result.Uids[0], nil
|
|
}
|
|
|
|
return 0, badger.ErrKeyNotFound
|
|
}
|
|
|
|
func (lc *LocalCache) getNoStore(key string) *List {
|
|
lc.RLock()
|
|
defer lc.RUnlock()
|
|
if l, ok := lc.plists[key]; ok {
|
|
return l
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetIfAbsent adds the list for the specified key to the cache. If a list for the same
|
|
// key already exists, the cache will not be modified and the existing list
|
|
// will be returned instead. This behavior is meant to prevent the goroutines
|
|
// using the cache from ending up with an orphaned version of a list.
|
|
func (lc *LocalCache) SetIfAbsent(key string, updated *List) *List {
|
|
lc.Lock()
|
|
defer lc.Unlock()
|
|
if pl, ok := lc.plists[key]; ok {
|
|
return pl
|
|
}
|
|
lc.plists[key] = updated
|
|
return updated
|
|
}
|
|
|
|
func (lc *LocalCache) getInternal(key []byte, readFromDisk, readUids bool) (*List, error) {
|
|
skey := string(key)
|
|
getNewPlistNil := func() (*List, error) {
|
|
lc.RLock()
|
|
defer lc.RUnlock()
|
|
if lc.plists == nil {
|
|
return getNew(key, pstore, lc.startTs, readUids)
|
|
}
|
|
if l, ok := lc.plists[skey]; ok {
|
|
return l, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
if l, err := getNewPlistNil(); l != nil || err != nil {
|
|
return l, err
|
|
}
|
|
|
|
var pl *List
|
|
if readFromDisk {
|
|
var err error
|
|
pl, err = getNew(key, pstore, lc.startTs, readUids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
pl = &List{
|
|
key: key,
|
|
plist: new(pb.PostingList),
|
|
mutationMap: newMutableLayer(),
|
|
}
|
|
}
|
|
|
|
// If we just brought this posting list into memory and we already have a delta for it, let's
|
|
// apply it before returning the list.
|
|
lc.RLock()
|
|
if delta, ok := lc.deltas[skey]; ok && len(delta) > 0 {
|
|
pl.setMutation(lc.startTs, delta)
|
|
}
|
|
lc.RUnlock()
|
|
return lc.SetIfAbsent(skey, pl), nil
|
|
}
|
|
|
|
func (lc *LocalCache) readPostingListAt(key []byte) (*pb.PostingList, error) {
|
|
if EnableDetailedMetrics {
|
|
start := time.Now()
|
|
defer func() {
|
|
ms := x.SinceMs(start)
|
|
pk, _ := x.Parse(key)
|
|
var tags []tag.Mutator
|
|
tags = append(tags, tag.Upsert(x.KeyMethod, "get"))
|
|
tags = append(tags, tag.Upsert(x.KeyStatus, pk.Attr))
|
|
_ = ostats.RecordWithTags(context.Background(), tags, x.BadgerReadLatencyMs.M(ms))
|
|
}()
|
|
}
|
|
|
|
pl := &pb.PostingList{}
|
|
txn := pstore.NewTransactionAt(lc.startTs, false)
|
|
defer txn.Discard()
|
|
|
|
item, err := txn.Get(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = item.Value(func(val []byte) error {
|
|
return proto.Unmarshal(val, pl)
|
|
})
|
|
|
|
return pl, err
|
|
}
|
|
|
|
// GetSinglePosting retrieves the cached version of the first item in the list associated with the
|
|
// given key. This is used for retrieving the value of a scalar predicats.
|
|
func (lc *LocalCache) GetSinglePosting(key []byte) (*pb.PostingList, error) {
|
|
// This would return an error if there is some data in the local cache, but we couldn't read it.
|
|
getListFromLocalCache := func() (*pb.PostingList, error) {
|
|
lc.RLock()
|
|
|
|
pl := &pb.PostingList{}
|
|
if delta, ok := lc.deltas[string(key)]; ok && len(delta) > 0 {
|
|
err := proto.Unmarshal(delta, pl)
|
|
lc.RUnlock()
|
|
return pl, err
|
|
}
|
|
|
|
l := lc.plists[string(key)]
|
|
lc.RUnlock()
|
|
|
|
if l != nil {
|
|
return l.StaticValue(lc.startTs)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
getPostings := func() (*pb.PostingList, error) {
|
|
pl, err := getListFromLocalCache()
|
|
// If both pl and err are empty, that means that there was no data in local cache, hence we should
|
|
// read the data from badger.
|
|
if pl != nil || err != nil {
|
|
return pl, err
|
|
}
|
|
|
|
return lc.readPostingListAt(key)
|
|
}
|
|
|
|
pl, err := getPostings()
|
|
if err == badger.ErrKeyNotFound {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Filter and remove STAR_ALL and OP_DELETE Postings
|
|
idx := 0
|
|
for _, postings := range pl.Postings {
|
|
if hasDeleteAll(postings) {
|
|
return nil, nil
|
|
}
|
|
if postings.Op != Del {
|
|
pl.Postings[idx] = postings
|
|
idx++
|
|
}
|
|
}
|
|
pl.Postings = pl.Postings[:idx]
|
|
return pl, nil
|
|
}
|
|
|
|
// Get retrieves the cached version of the list associated with the given key.
|
|
func (lc *LocalCache) Get(key []byte) (*List, error) {
|
|
return lc.getInternal(key, true, false)
|
|
}
|
|
|
|
func (lc *LocalCache) GetUids(key []byte) (*List, error) {
|
|
return lc.getInternal(key, true, true)
|
|
}
|
|
|
|
// GetFromDelta gets the cached version of the list without reading from disk
|
|
// and only applies the existing deltas. This is used in situations where the
|
|
// posting list will only be modified and not read (e.g adding index mutations).
|
|
func (lc *LocalCache) GetFromDelta(key []byte) (*List, error) {
|
|
return lc.getInternal(key, false, false)
|
|
}
|
|
|
|
// UpdateDeltasAndDiscardLists updates the delta cache before removing the stored posting lists.
|
|
func (lc *LocalCache) UpdateDeltasAndDiscardLists() {
|
|
lc.Lock()
|
|
defer lc.Unlock()
|
|
if len(lc.plists) == 0 {
|
|
return
|
|
}
|
|
|
|
for key, pl := range lc.plists {
|
|
data := pl.getMutation(lc.startTs)
|
|
if len(data) > 0 {
|
|
lc.deltas[key] = data
|
|
}
|
|
lc.maxVersions[key] = pl.maxVersion()
|
|
// We can't run pl.release() here because LocalCache is still being used by other callers
|
|
// for the same transaction, who might be holding references to posting lists.
|
|
// TODO: Find another way to reuse postings via postingPool.
|
|
}
|
|
lc.plists = make(map[string]*List)
|
|
}
|
|
|
|
func (lc *LocalCache) fillPreds(ctx *api.TxnContext, gid uint32) {
|
|
lc.RLock()
|
|
defer lc.RUnlock()
|
|
for key := range lc.deltas {
|
|
pk, err := x.Parse([]byte(key))
|
|
x.Check(err)
|
|
if len(pk.Attr) == 0 {
|
|
continue
|
|
}
|
|
// Also send the group id that the predicate was being served by. This is useful when
|
|
// checking if Zero should allow a commit during a predicate move.
|
|
predKey := fmt.Sprintf("%d-%s", gid, pk.Attr)
|
|
ctx.Preds = append(ctx.Preds, predKey)
|
|
}
|
|
ctx.Preds = x.Unique(ctx.Preds)
|
|
}
|