420 lines
11 KiB
Go
420 lines
11 KiB
Go
|
package fastcache
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
"runtime"
|
||
|
|
||
|
"github.com/golang/snappy"
|
||
|
)
|
||
|
|
||
|
// SaveToFile atomically saves cache data to the given filePath using a single
|
||
|
// CPU core.
|
||
|
//
|
||
|
// SaveToFile may be called concurrently with other operations on the cache.
|
||
|
//
|
||
|
// The saved data may be loaded with LoadFromFile*.
|
||
|
//
|
||
|
// See also SaveToFileConcurrent for faster saving to file.
|
||
|
func (c *Cache) SaveToFile(filePath string) error {
|
||
|
return c.SaveToFileConcurrent(filePath, 1)
|
||
|
}
|
||
|
|
||
|
// SaveToFileConcurrent saves cache data to the given filePath using concurrency
|
||
|
// CPU cores.
|
||
|
//
|
||
|
// SaveToFileConcurrent may be called concurrently with other operations
|
||
|
// on the cache.
|
||
|
//
|
||
|
// The saved data may be loaded with LoadFromFile*.
|
||
|
//
|
||
|
// See also SaveToFile.
|
||
|
func (c *Cache) SaveToFileConcurrent(filePath string, concurrency int) error {
|
||
|
// Create dir if it doesn't exist.
|
||
|
dir := filepath.Dir(filePath)
|
||
|
if _, err := os.Stat(dir); err != nil {
|
||
|
if !os.IsNotExist(err) {
|
||
|
return fmt.Errorf("cannot stat %q: %s", dir, err)
|
||
|
}
|
||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
|
return fmt.Errorf("cannot create dir %q: %s", dir, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Save cache data into a temporary directory.
|
||
|
tmpDir, err := ioutil.TempDir(dir, "fastcache.tmp.")
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot create temporary dir inside %q: %s", dir, err)
|
||
|
}
|
||
|
defer func() {
|
||
|
if tmpDir != "" {
|
||
|
_ = os.RemoveAll(tmpDir)
|
||
|
}
|
||
|
}()
|
||
|
gomaxprocs := runtime.GOMAXPROCS(-1)
|
||
|
if concurrency <= 0 || concurrency > gomaxprocs {
|
||
|
concurrency = gomaxprocs
|
||
|
}
|
||
|
if err := c.save(tmpDir, concurrency); err != nil {
|
||
|
return fmt.Errorf("cannot save cache data to temporary dir %q: %s", tmpDir, err)
|
||
|
}
|
||
|
|
||
|
// Remove old filePath contents, since os.Rename may return
|
||
|
// error if filePath dir exists.
|
||
|
if err := os.RemoveAll(filePath); err != nil {
|
||
|
return fmt.Errorf("cannot remove old contents at %q: %s", filePath, err)
|
||
|
}
|
||
|
if err := os.Rename(tmpDir, filePath); err != nil {
|
||
|
return fmt.Errorf("cannot move temporary dir %q to %q: %s", tmpDir, filePath, err)
|
||
|
}
|
||
|
tmpDir = ""
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// LoadFromFile loads cache data from the given filePath.
|
||
|
//
|
||
|
// See SaveToFile* for saving cache data to file.
|
||
|
func LoadFromFile(filePath string) (*Cache, error) {
|
||
|
return load(filePath, 0)
|
||
|
}
|
||
|
|
||
|
// LoadFromFileOrNew tries loading cache data from the given filePath.
|
||
|
//
|
||
|
// The function falls back to creating new cache with the given maxBytes
|
||
|
// capacity if error occurs during loading the cache from file.
|
||
|
func LoadFromFileOrNew(filePath string, maxBytes int) *Cache {
|
||
|
c, err := load(filePath, maxBytes)
|
||
|
if err == nil {
|
||
|
return c
|
||
|
}
|
||
|
return New(maxBytes)
|
||
|
}
|
||
|
|
||
|
func (c *Cache) save(dir string, workersCount int) error {
|
||
|
if err := saveMetadata(c, dir); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Save buckets by workersCount concurrent workers.
|
||
|
workCh := make(chan int, workersCount)
|
||
|
results := make(chan error)
|
||
|
for i := 0; i < workersCount; i++ {
|
||
|
go func(workerNum int) {
|
||
|
results <- saveBuckets(c.buckets[:], workCh, dir, workerNum)
|
||
|
}(i)
|
||
|
}
|
||
|
// Feed workers with work
|
||
|
for i := range c.buckets[:] {
|
||
|
workCh <- i
|
||
|
}
|
||
|
close(workCh)
|
||
|
|
||
|
// Read results.
|
||
|
var err error
|
||
|
for i := 0; i < workersCount; i++ {
|
||
|
result := <-results
|
||
|
if result != nil && err == nil {
|
||
|
err = result
|
||
|
}
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func load(filePath string, maxBytes int) (*Cache, error) {
|
||
|
maxBucketChunks, err := loadMetadata(filePath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if maxBytes > 0 {
|
||
|
maxBucketBytes := uint64((maxBytes + bucketsCount - 1) / bucketsCount)
|
||
|
expectedBucketChunks := (maxBucketBytes + chunkSize - 1) / chunkSize
|
||
|
if maxBucketChunks != expectedBucketChunks {
|
||
|
return nil, fmt.Errorf("cache file %s contains maxBytes=%d; want %d", filePath, maxBytes, expectedBucketChunks*chunkSize*bucketsCount)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read bucket files from filePath dir.
|
||
|
d, err := os.Open(filePath)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("cannot open %q: %s", filePath, err)
|
||
|
}
|
||
|
defer func() {
|
||
|
_ = d.Close()
|
||
|
}()
|
||
|
fis, err := d.Readdir(-1)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("cannot read files from %q: %s", filePath, err)
|
||
|
}
|
||
|
results := make(chan error)
|
||
|
workersCount := 0
|
||
|
var c Cache
|
||
|
for _, fi := range fis {
|
||
|
fn := fi.Name()
|
||
|
if fi.IsDir() || !dataFileRegexp.MatchString(fn) {
|
||
|
continue
|
||
|
}
|
||
|
workersCount++
|
||
|
go func(dataPath string) {
|
||
|
results <- loadBuckets(c.buckets[:], dataPath, maxBucketChunks)
|
||
|
}(filePath + "/" + fn)
|
||
|
}
|
||
|
err = nil
|
||
|
for i := 0; i < workersCount; i++ {
|
||
|
result := <-results
|
||
|
if result != nil && err == nil {
|
||
|
err = result
|
||
|
}
|
||
|
}
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
// Initialize buckets, which could be missing due to incomplete or corrupted files in the cache.
|
||
|
// It is better initializing such buckets instead of returning error, since the rest of buckets
|
||
|
// contain valid data.
|
||
|
for i := range c.buckets[:] {
|
||
|
b := &c.buckets[i]
|
||
|
if len(b.chunks) == 0 {
|
||
|
b.chunks = make([][]byte, maxBucketChunks)
|
||
|
b.m = make(map[uint64]uint64)
|
||
|
}
|
||
|
}
|
||
|
return &c, nil
|
||
|
}
|
||
|
|
||
|
func saveMetadata(c *Cache, dir string) error {
|
||
|
metadataPath := dir + "/metadata.bin"
|
||
|
metadataFile, err := os.Create(metadataPath)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot create %q: %s", metadataPath, err)
|
||
|
}
|
||
|
defer func() {
|
||
|
_ = metadataFile.Close()
|
||
|
}()
|
||
|
maxBucketChunks := uint64(cap(c.buckets[0].chunks))
|
||
|
if err := writeUint64(metadataFile, maxBucketChunks); err != nil {
|
||
|
return fmt.Errorf("cannot write maxBucketChunks=%d to %q: %s", maxBucketChunks, metadataPath, err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func loadMetadata(dir string) (uint64, error) {
|
||
|
metadataPath := dir + "/metadata.bin"
|
||
|
metadataFile, err := os.Open(metadataPath)
|
||
|
if err != nil {
|
||
|
return 0, fmt.Errorf("cannot open %q: %s", metadataPath, err)
|
||
|
}
|
||
|
defer func() {
|
||
|
_ = metadataFile.Close()
|
||
|
}()
|
||
|
maxBucketChunks, err := readUint64(metadataFile)
|
||
|
if err != nil {
|
||
|
return 0, fmt.Errorf("cannot read maxBucketChunks from %q: %s", metadataPath, err)
|
||
|
}
|
||
|
if maxBucketChunks == 0 {
|
||
|
return 0, fmt.Errorf("invalid maxBucketChunks=0 read from %q", metadataPath)
|
||
|
}
|
||
|
return maxBucketChunks, nil
|
||
|
}
|
||
|
|
||
|
var dataFileRegexp = regexp.MustCompile(`^data\.\d+\.bin$`)
|
||
|
|
||
|
func saveBuckets(buckets []bucket, workCh <-chan int, dir string, workerNum int) error {
|
||
|
dataPath := fmt.Sprintf("%s/data.%d.bin", dir, workerNum)
|
||
|
dataFile, err := os.Create(dataPath)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot create %q: %s", dataPath, err)
|
||
|
}
|
||
|
defer func() {
|
||
|
_ = dataFile.Close()
|
||
|
}()
|
||
|
zw := snappy.NewBufferedWriter(dataFile)
|
||
|
for bucketNum := range workCh {
|
||
|
if err := writeUint64(zw, uint64(bucketNum)); err != nil {
|
||
|
return fmt.Errorf("cannot write bucketNum=%d to %q: %s", bucketNum, dataPath, err)
|
||
|
}
|
||
|
if err := buckets[bucketNum].Save(zw); err != nil {
|
||
|
return fmt.Errorf("cannot save bucket[%d] to %q: %s", bucketNum, dataPath, err)
|
||
|
}
|
||
|
}
|
||
|
if err := zw.Close(); err != nil {
|
||
|
return fmt.Errorf("cannot close snappy.Writer for %q: %s", dataPath, err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func loadBuckets(buckets []bucket, dataPath string, maxChunks uint64) error {
|
||
|
dataFile, err := os.Open(dataPath)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot open %q: %s", dataPath, err)
|
||
|
}
|
||
|
defer func() {
|
||
|
_ = dataFile.Close()
|
||
|
}()
|
||
|
zr := snappy.NewReader(dataFile)
|
||
|
for {
|
||
|
bucketNum, err := readUint64(zr)
|
||
|
if err == io.EOF {
|
||
|
// Reached the end of file.
|
||
|
return nil
|
||
|
}
|
||
|
if bucketNum >= uint64(len(buckets)) {
|
||
|
return fmt.Errorf("unexpected bucketNum read from %q: %d; must be smaller than %d", dataPath, bucketNum, len(buckets))
|
||
|
}
|
||
|
if err := buckets[bucketNum].Load(zr, maxChunks); err != nil {
|
||
|
return fmt.Errorf("cannot load bucket[%d] from %q: %s", bucketNum, dataPath, err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (b *bucket) Save(w io.Writer) error {
|
||
|
b.Clean()
|
||
|
|
||
|
b.mu.RLock()
|
||
|
defer b.mu.RUnlock()
|
||
|
|
||
|
// Store b.idx, b.gen and b.m to w.
|
||
|
|
||
|
bIdx := b.idx
|
||
|
bGen := b.gen
|
||
|
chunksLen := 0
|
||
|
for _, chunk := range b.chunks {
|
||
|
if chunk == nil {
|
||
|
break
|
||
|
}
|
||
|
chunksLen++
|
||
|
}
|
||
|
kvs := make([]byte, 0, 2*8*len(b.m))
|
||
|
var u64Buf [8]byte
|
||
|
for k, v := range b.m {
|
||
|
binary.LittleEndian.PutUint64(u64Buf[:], k)
|
||
|
kvs = append(kvs, u64Buf[:]...)
|
||
|
binary.LittleEndian.PutUint64(u64Buf[:], v)
|
||
|
kvs = append(kvs, u64Buf[:]...)
|
||
|
}
|
||
|
|
||
|
if err := writeUint64(w, bIdx); err != nil {
|
||
|
return fmt.Errorf("cannot write b.idx: %s", err)
|
||
|
}
|
||
|
if err := writeUint64(w, bGen); err != nil {
|
||
|
return fmt.Errorf("cannot write b.gen: %s", err)
|
||
|
}
|
||
|
if err := writeUint64(w, uint64(len(kvs))/2/8); err != nil {
|
||
|
return fmt.Errorf("cannot write len(b.m): %s", err)
|
||
|
}
|
||
|
if _, err := w.Write(kvs); err != nil {
|
||
|
return fmt.Errorf("cannot write b.m: %s", err)
|
||
|
}
|
||
|
|
||
|
// Store b.chunks to w.
|
||
|
if err := writeUint64(w, uint64(chunksLen)); err != nil {
|
||
|
return fmt.Errorf("cannot write len(b.chunks): %s", err)
|
||
|
}
|
||
|
for chunkIdx := 0; chunkIdx < chunksLen; chunkIdx++ {
|
||
|
chunk := b.chunks[chunkIdx][:chunkSize]
|
||
|
if _, err := w.Write(chunk); err != nil {
|
||
|
return fmt.Errorf("cannot write b.chunks[%d]: %s", chunkIdx, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (b *bucket) Load(r io.Reader, maxChunks uint64) error {
|
||
|
if maxChunks == 0 {
|
||
|
return fmt.Errorf("the number of chunks per bucket cannot be zero")
|
||
|
}
|
||
|
bIdx, err := readUint64(r)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot read b.idx: %s", err)
|
||
|
}
|
||
|
bGen, err := readUint64(r)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot read b.gen: %s", err)
|
||
|
}
|
||
|
kvsLen, err := readUint64(r)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot read len(b.m): %s", err)
|
||
|
}
|
||
|
kvsLen *= 2 * 8
|
||
|
kvs := make([]byte, kvsLen)
|
||
|
if _, err := io.ReadFull(r, kvs); err != nil {
|
||
|
return fmt.Errorf("cannot read b.m: %s", err)
|
||
|
}
|
||
|
m := make(map[uint64]uint64, kvsLen/2/8)
|
||
|
for len(kvs) > 0 {
|
||
|
k := binary.LittleEndian.Uint64(kvs)
|
||
|
kvs = kvs[8:]
|
||
|
v := binary.LittleEndian.Uint64(kvs)
|
||
|
kvs = kvs[8:]
|
||
|
m[k] = v
|
||
|
}
|
||
|
|
||
|
maxBytes := maxChunks * chunkSize
|
||
|
if maxBytes >= maxBucketSize {
|
||
|
return fmt.Errorf("too big maxBytes=%d; should be smaller than %d", maxBytes, maxBucketSize)
|
||
|
}
|
||
|
chunks := make([][]byte, maxChunks)
|
||
|
chunksLen, err := readUint64(r)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("cannot read len(b.chunks): %s", err)
|
||
|
}
|
||
|
if chunksLen > uint64(maxChunks) {
|
||
|
return fmt.Errorf("chunksLen=%d cannot exceed maxChunks=%d", chunksLen, maxChunks)
|
||
|
}
|
||
|
currChunkIdx := bIdx / chunkSize
|
||
|
if currChunkIdx > 0 && currChunkIdx >= chunksLen {
|
||
|
return fmt.Errorf("too big bIdx=%d; should be smaller than %d", bIdx, chunksLen*chunkSize)
|
||
|
}
|
||
|
for chunkIdx := uint64(0); chunkIdx < chunksLen; chunkIdx++ {
|
||
|
chunk := getChunk()
|
||
|
chunks[chunkIdx] = chunk
|
||
|
if _, err := io.ReadFull(r, chunk); err != nil {
|
||
|
// Free up allocated chunks before returning the error.
|
||
|
for _, chunk := range chunks {
|
||
|
if chunk != nil {
|
||
|
putChunk(chunk)
|
||
|
}
|
||
|
}
|
||
|
return fmt.Errorf("cannot read b.chunks[%d]: %s", chunkIdx, err)
|
||
|
}
|
||
|
}
|
||
|
// Adjust len for the chunk pointed by currChunkIdx.
|
||
|
if chunksLen > 0 {
|
||
|
chunkLen := bIdx % chunkSize
|
||
|
chunks[currChunkIdx] = chunks[currChunkIdx][:chunkLen]
|
||
|
}
|
||
|
|
||
|
b.mu.Lock()
|
||
|
for _, chunk := range b.chunks {
|
||
|
putChunk(chunk)
|
||
|
}
|
||
|
b.chunks = chunks
|
||
|
b.m = m
|
||
|
b.idx = bIdx
|
||
|
b.gen = bGen
|
||
|
b.mu.Unlock()
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func writeUint64(w io.Writer, u uint64) error {
|
||
|
var u64Buf [8]byte
|
||
|
binary.LittleEndian.PutUint64(u64Buf[:], u)
|
||
|
_, err := w.Write(u64Buf[:])
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func readUint64(r io.Reader) (uint64, error) {
|
||
|
var u64Buf [8]byte
|
||
|
if _, err := io.ReadFull(r, u64Buf[:]); err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
u := binary.LittleEndian.Uint64(u64Buf[:])
|
||
|
return u, nil
|
||
|
}
|