github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/store/nbs/ghost_store.go (about)

     1  // Copyright 2024 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package nbs
    16  
    17  import (
    18  	"bufio"
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"io/fs"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	"github.com/dolthub/dolt/go/store/chunks"
    27  	"github.com/dolthub/dolt/go/store/hash"
    28  )
    29  
    30  type GhostBlockStore struct {
    31  	skippedRefs      *hash.HashSet
    32  	ghostObjectsFile string
    33  }
    34  
    35  // We use the Has, HasMany, Get, GetMany, and PersistGhostHashes methods from the ChunkStore interface. All other methods are not supported.
    36  var _ chunks.ChunkStore = &GhostBlockStore{}
    37  
    38  // NewGhostBlockStore returns a new GhostBlockStore instance. Currently the only parameter is the path to the directory
    39  // where we will create a text file called ghostObjects.txt. This file will contain the hashes of the ghost objects. Creation
    40  // and use of this file is constrained to this instance. If there is no ghostObjects.txt file, then the GhostBlockStore will
    41  // be empty - never returning any values from the Has, HasMany, Get, or GetMany methods.
    42  func NewGhostBlockStore(nomsPath string) (*GhostBlockStore, error) {
    43  	ghostPath := filepath.Join(nomsPath, "ghostObjects.txt")
    44  	f, err := os.Open(ghostPath)
    45  	if err != nil {
    46  		if errors.Is(err, fs.ErrNotExist) {
    47  			return &GhostBlockStore{
    48  				skippedRefs:      &hash.HashSet{},
    49  				ghostObjectsFile: ghostPath,
    50  			}, nil
    51  		}
    52  		// Other error, permission denied, etc, we want to hear about.
    53  		return nil, err
    54  	}
    55  	scanner := bufio.NewScanner(f)
    56  	skiplist := &hash.HashSet{}
    57  	for scanner.Scan() {
    58  		h := scanner.Text()
    59  		if hash.IsValid(h) {
    60  			skiplist.Insert(hash.Parse(h))
    61  		} else {
    62  			return nil, fmt.Errorf("invalid hash %s in ghostObjects.txt", h)
    63  		}
    64  	}
    65  
    66  	return &GhostBlockStore{
    67  		skippedRefs:      skiplist,
    68  		ghostObjectsFile: ghostPath,
    69  	}, nil
    70  }
    71  
    72  // Get returns a ghost chunk if the hash is in the ghostObjectsFile. Otherwise, it returns an empty chunk. Chunks returned
    73  // by this code will always be ghost chunks, ie chunk.IsGhost() will always return true.
    74  func (g GhostBlockStore) Get(ctx context.Context, h hash.Hash) (chunks.Chunk, error) {
    75  	if g.skippedRefs.Has(h) {
    76  		return *chunks.NewGhostChunk(h), nil
    77  	}
    78  	return chunks.EmptyChunk, nil
    79  }
    80  
    81  func (g GhostBlockStore) GetMany(ctx context.Context, hashes hash.HashSet, found func(context.Context, *chunks.Chunk)) error {
    82  	for h := range hashes {
    83  		if g.skippedRefs.Has(h) {
    84  			found(ctx, chunks.NewGhostChunk(h))
    85  		}
    86  	}
    87  	return nil
    88  }
    89  
    90  func (g *GhostBlockStore) PersistGhostHashes(ctx context.Context, hashes hash.HashSet) error {
    91  	if hashes.Size() == 0 {
    92  		return fmt.Errorf("runtime error. PersistGhostHashes called with empty hash set")
    93  	}
    94  
    95  	f, err := os.OpenFile(g.ghostObjectsFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	for h := range hashes {
   101  		if _, err := f.WriteString(h.String() + "\n"); err != nil {
   102  			return err
   103  		}
   104  	}
   105  
   106  	g.skippedRefs = &hash.HashSet{}
   107  	for h := range hashes {
   108  		g.skippedRefs.Insert(h)
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  func (g GhostBlockStore) Has(ctx context.Context, h hash.Hash) (bool, error) {
   115  	if g.skippedRefs.Has(h) {
   116  		return true, nil
   117  	}
   118  	return false, nil
   119  }
   120  
   121  func (g GhostBlockStore) HasMany(ctx context.Context, hashes hash.HashSet) (absent hash.HashSet, err error) {
   122  	return g.hasMany(hashes)
   123  }
   124  
   125  func (g GhostBlockStore) hasMany(hashes hash.HashSet) (absent hash.HashSet, err error) {
   126  	absent = hash.HashSet{}
   127  	for h := range hashes {
   128  		if !g.skippedRefs.Has(h) {
   129  			absent.Insert(h)
   130  		}
   131  	}
   132  	return absent, nil
   133  }
   134  
   135  func (g GhostBlockStore) Put(ctx context.Context, c chunks.Chunk, getAddrs chunks.GetAddrsCurry) error {
   136  	panic("GhostBlockStore does not support Put")
   137  }
   138  
   139  func (g GhostBlockStore) Version() string {
   140  	panic("GhostBlockStore does not support Version")
   141  }
   142  
   143  func (g GhostBlockStore) AccessMode() chunks.ExclusiveAccessMode {
   144  	panic("GhostBlockStore does not support AccessMode")
   145  }
   146  
   147  func (g GhostBlockStore) Rebase(ctx context.Context) error {
   148  	panic("GhostBlockStore does not support Rebase")
   149  }
   150  
   151  func (g GhostBlockStore) Root(ctx context.Context) (hash.Hash, error) {
   152  	panic("GhostBlockStore does not support Root")
   153  }
   154  
   155  func (g GhostBlockStore) Commit(ctx context.Context, current, last hash.Hash) (bool, error) {
   156  	panic("GhostBlockStore does not support Commit")
   157  }
   158  
   159  func (g GhostBlockStore) Stats() interface{} {
   160  	panic("GhostBlockStore does not support Stats")
   161  }
   162  
   163  func (g GhostBlockStore) StatsSummary() string {
   164  	panic("GhostBlockStore does not support StatsSummary")
   165  }
   166  
   167  func (g GhostBlockStore) Close() error {
   168  	panic("GhostBlockStore does not support Close")
   169  }