featurebase/translate.go
Ben Johnson 7ff684c193
Allow partial translate file reads.
This commit fixes an issue where translation `LogEntry` must be
read in its entirety, however, large entries can exceed the buffer
size. This has been changed so that partial entries reads are allowed.

The `LogEntry.ReadFrom()` may still generate large byte slices
during reads of large individual fields or keys.
2019-06-04 10:08:52 -06:00

1167 lines
29 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 (
"bufio"
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sync"
"syscall"
"time"
"github.com/cespare/xxhash"
"github.com/pilosa/pilosa/logger"
"github.com/pkg/errors"
)
// Log entry type constants.
const (
LogEntryTypeInsertColumn = 1
LogEntryTypeInsertRow = 2
)
const (
defaultReplicationRetryInterval = 1 * time.Second
)
// Translate store errors.
var (
ErrTranslateStoreClosed = errors.New("pilosa: translate store closed")
ErrTranslateStoreReaderClosed = errors.New("pilosa: translate store reader closed")
ErrReplicationNotSupported = errors.New("pilosa: replication not supported")
ErrTranslateStoreReadOnly = errors.New("pilosa: translate store could not find or create key, translate store read only")
)
// TranslateStore is the storage for translation string-to-uint64 values.
type TranslateStore interface {
TranslateColumnsToUint64(index string, values []string) ([]uint64, error)
TranslateColumnToString(index string, values uint64) (string, error)
TranslateRowsToUint64(index, field string, values []string) ([]uint64, error)
TranslateRowToString(index, field string, values uint64) (string, error)
// Returns a reader from the given offset of the raw data file.
// The returned reader must be closed by the caller when done.
Reader(ctx context.Context, off int64) (io.ReadCloser, error)
}
// Ensure type implements interface.
var _ TranslateStore = &TranslateFile{}
// TranslateFile is an on-disk storage engine for translating string-to-uint64 values.
type TranslateFile struct {
mu sync.RWMutex
data []byte
file *os.File
w *bufio.Writer
n int64
writeNotify chan struct{}
once sync.Once
wg sync.WaitGroup
closing chan struct{}
cols map[string]*index
rows map[fieldKey]*index
Path string
mapSize int
logger logger.Logger
// If non-nil, data is streamed from a primary and this is a read-only store.
PrimaryTranslateStore TranslateStore
primaryID string // unique ID used to identify the primary store
replicationClosing chan struct{}
primaryStoreEvents chan primaryStoreEvent
repWG sync.WaitGroup
// Delay after attempting to connect to a primary that the store will retry.
replicationRetryInterval time.Duration
}
// TranslateFileOption is a functional option type for pilosa.TranslateFile
type TranslateFileOption func(f *TranslateFile) error
// OptTranslateFileMapSize is a functional option on TranslateFile
// used to set the map size.
func OptTranslateFileMapSize(mapSize int) TranslateFileOption {
return func(f *TranslateFile) error {
f.mapSize = mapSize
return nil
}
}
// OptTranslateFileLogger is a functional option on TranslateFile
// used to set the file logger.
func OptTranslateFileLogger(l logger.Logger) TranslateFileOption {
return func(s *TranslateFile) error {
s.logger = l
return nil
}
}
// NewTranslateFile returns a new instance of TranslateFile.
func NewTranslateFile(opts ...TranslateFileOption) *TranslateFile {
var defaultMapSize64 int64 = 10 * (1 << 30)
var defaultMapSize int
if ^uint(0)>>32 > 0 {
// 10GB default map size
defaultMapSize = int(defaultMapSize64)
} else {
// Use 2GB default map size on 32-bit systems
defaultMapSize = (1 << 31) - 1
}
f := &TranslateFile{
writeNotify: make(chan struct{}),
closing: make(chan struct{}),
cols: make(map[string]*index),
rows: make(map[fieldKey]*index),
mapSize: defaultMapSize,
logger: logger.NopLogger,
replicationClosing: make(chan struct{}),
primaryStoreEvents: make(chan primaryStoreEvent),
replicationRetryInterval: defaultReplicationRetryInterval,
}
for _, opt := range opts {
err := opt(f)
if err != nil {
// TODO (2.0): Change func signature to return error
panic(errors.Wrap(err, "applying option"))
}
}
return f
}
// Open opens the translate file.
func (s *TranslateFile) Open() (err error) {
// Open writer & buffered writer.
if err := os.MkdirAll(filepath.Dir(s.Path), 0777); err != nil {
return errors.Wrapf(err, "mkdir %s", filepath.Dir(s.Path))
} else if s.file, err = os.OpenFile(s.Path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666); err != nil {
return errors.Wrapf(err, "open file %s", s.Path)
}
s.w = bufio.NewWriter(s.file)
s.n = 0
// Memory map data file.
if s.data, err = syscall.Mmap(int(s.file.Fd()), 0, s.mapSize, syscall.PROT_READ, syscall.MAP_SHARED); err != nil {
return errors.Wrapf(err, "creating Mmap (size: %d)", s.mapSize)
}
// Replay the log.
if err := s.replayEntries(); err != nil {
return errors.Wrap(err, "replaying log entries")
}
// Listen to primaryStoreEvents channel.
s.wg.Add(1)
go func() { defer s.wg.Done(); s.monitorPrimaryStoreEvents() }()
return nil
}
// primaryStoreEvent is used to set/change the primary translate store.
// It contains a TranslateStore along with an associated string ID which
// is used to determine whether the primary needs to be changed from the
// current value.
type primaryStoreEvent struct {
id string
ts TranslateStore
}
// SetPrimaryStore sets the translate files's primary translate store.
// The id value is used to determine whether the primary needs to be changed
// from the current value (i.e. calling this multiple times with the same
// input values will no-op on all subsequent calls).
func (s *TranslateFile) SetPrimaryStore(id string, ts TranslateStore) {
go func() {
s.primaryStoreEvents <- primaryStoreEvent{
id: id,
ts: ts,
}
}()
}
// handlePrimaryStoreEvent changes the PrimaryTranslateStore
// used for replication by TranslateFile.
func (s *TranslateFile) handlePrimaryStoreEvent(ev primaryStoreEvent) error {
s.mu.Lock()
defer s.mu.Unlock()
if ev.id == s.primaryID {
return nil
}
// Stop translate store replication.
close(s.replicationClosing)
s.repWG.Wait()
// Set the primary node for translate store replication.
s.logger.Debugf("set primary translate store to %s", ev.id)
s.primaryID = ev.id
if ev.id == "" {
s.PrimaryTranslateStore = nil
} else {
s.PrimaryTranslateStore = ev.ts
}
// Start translate store replication. Stream from primary, if available.
if s.PrimaryTranslateStore != nil {
s.replicationClosing = make(chan struct{})
s.repWG.Add(1)
go func() { defer s.repWG.Done(); s.monitorReplication() }()
}
return nil
}
// Close closes the translate file.
func (s *TranslateFile) Close() (err error) {
s.once.Do(func() {
close(s.closing)
if s.file != nil {
if e := s.file.Close(); e != nil && err == nil {
err = e
}
}
if s.data != nil {
if e := syscall.Munmap(s.data); e != nil && err == nil {
err = e
}
}
})
s.wg.Wait()
return err
}
// Closing returns a channel that is closed when the store is closed.
func (s *TranslateFile) Closing() <-chan struct{} {
return s.closing
}
// size returns the number of bytes in use in the data file.
func (s *TranslateFile) size() int64 {
s.mu.RLock()
n := s.n
s.mu.RUnlock()
return n
}
// isReadOnly returns true if this store is being replicated from a primary store.
func (s *TranslateFile) isReadOnly() bool {
return s.PrimaryTranslateStore != nil
}
// WriteNotify returns a channel that is closed when a new entry is written.
func (s *TranslateFile) WriteNotify() <-chan struct{} {
s.mu.RLock()
ch := s.writeNotify
s.mu.RUnlock()
return ch
}
func (s *TranslateFile) appendEntry(entry *LogEntry) error {
offset := s.n
// Append entry to the end of the WAL.
n, err := entry.WriteTo(s.w)
if err != nil {
return err
} else if err := s.w.Flush(); err != nil {
return err
}
// Move position forward.
s.n += n
// Apply the entry to the current state.
if err := s.applyEntry(entry, offset); err != nil {
return err
} else if err := s.file.Sync(); err != nil {
return err
}
// Notify others of write update.
close(s.writeNotify)
s.writeNotify = make(chan struct{})
return nil
}
func (s *TranslateFile) applyEntry(entry *LogEntry, offset int64) error {
// Move offset to the start of the id/key pairs.
offset += entry.headerSize()
var idx *index
switch entry.Type {
case LogEntryTypeInsertColumn:
idx = s.col(string(entry.Index))
case LogEntryTypeInsertRow:
idx = s.row(string(entry.Index), string(entry.Field))
default:
return fmt.Errorf("enterprise.TranslateFile.applyEntry(): unknown log entry type: 0x%20x", entry.Type)
}
// Insert id/key pairs into index.
for i, id := range entry.IDs {
key := entry.Keys[i]
// Determine key offset based on ID size.
sz := int64(uVarintSize(id))
idx.insert(id, offset+sz)
// Move sequence forward.
if id > idx.seq {
idx.seq = id
}
// Move offset forward.
offset += sz + int64(uVarintSize(uint64(len(key)))) + int64(len(key))
}
return nil
}
func (s *TranslateFile) replayEntries() error {
// Build a reader from the memory-map data.
fi, err := os.Stat(s.Path)
if err != nil {
return err
}
r := bytes.NewReader(s.data[:fi.Size()])
// Iterate over each entry and reapply.
for {
offset := s.n
var entry LogEntry
if n, err := entry.ReadFrom(r); err == io.EOF {
return nil
} else if err != nil {
return err
} else {
s.n += n
}
if err := s.applyEntry(&entry, offset); err != nil {
return err
}
}
}
// monitorReplication is executed in a separate goroutine and continually streams
// from the primary store until this store is closed.
func (s *TranslateFile) monitorReplication() {
// Create context that will cancel on close.
ctx, cancel := context.WithCancel(context.Background())
go func() {
select {
case <-s.closing:
case <-s.replicationClosing:
}
cancel()
}()
// Keep attempting to replicate until the store closes.
for {
if err := s.replicate(ctx); err != nil {
s.logger.Printf("pilosa: replication error: %s", err)
}
select {
case <-ctx.Done():
return
case <-time.After(s.replicationRetryInterval):
s.logger.Printf("pilosa: reconnecting to primary replica")
}
}
}
// monitorPrimaryStoreEvents is executed in a separate goroutine and listens for changes
// to the primary store assignment.
func (s *TranslateFile) monitorPrimaryStoreEvents() {
// Keep handling events until the store closes.
for {
select {
case <-s.closing:
return
case ev := <-s.primaryStoreEvents:
if err := s.handlePrimaryStoreEvent(ev); err != nil {
s.logger.Printf("handle primary store event")
}
}
}
}
func (s *TranslateFile) replicate(ctx context.Context) error {
off := s.size()
// Connect to remote primary.
s.logger.Debugf("pilosa: replicating from offset %d", off)
rc, err := s.PrimaryTranslateStore.Reader(ctx, off)
if err != nil {
return err
}
defer rc.Close()
// Wrap in bufferred I/O so it implements io.ByteReader.
bufr := bufio.NewReader(rc)
// we need a way to make an asynchronous routine hand us back an error,
// but we might not still be there to get it. so we have a buffer.
chErr := make(chan error, 1)
// Continually read new entries from primary and append to local store.
for {
// Read next available entry.
var entry LogEntry
if _, err = entry.ReadFrom(bufr); err == io.EOF {
return nil
} else if err != nil {
return err
}
// note: we should never end up spawning two of this goroutine
// at once. either we end up reading the error from chErr below,
// and this loop continues, or we don't, and the whole function
// returns. if the function returns, we can write that single
// error to the empty channel with a buffer of 1, the goroutine
// terminates, and chErr becomes garbage-collectable.
go func() {
s.mu.Lock()
defer s.mu.Unlock()
// Write to local store.
err = s.appendEntry(&entry)
chErr <- err
}()
select {
case err = <-chErr:
if err != nil {
return err
}
case <-s.replicationClosing:
return nil
case <-ctx.Done():
return nil
}
}
}
func (s *TranslateFile) col(index string) *index {
idx := s.cols[index]
if idx == nil {
idx = newIndex(s.data)
s.cols[index] = idx
}
return idx
}
func (s *TranslateFile) row(index, field string) *index {
idx := s.rows[fieldKey{index, field}]
if idx == nil {
idx = newIndex(s.data)
s.rows[fieldKey{index, field}] = idx
}
return idx
}
// TranslateColumnsToUint64 converts values to a uint64 id.
// If value does not have an associated id then one is created.
func (s *TranslateFile) TranslateColumnsToUint64(index string, values []string) ([]uint64, error) {
ret := make([]uint64, len(values))
// Read value under read lock.
s.mu.RLock()
if idx := s.cols[index]; idx != nil {
var writeRequired bool
for i := range values {
v, ok := idx.idByKey([]byte(values[i]))
if !ok {
writeRequired = true
}
ret[i] = v
}
if !writeRequired {
s.mu.RUnlock()
return ret, nil
}
}
s.mu.RUnlock()
// Return error if not all values could be translated and this store is read-only.
if s.isReadOnly() {
return ret, ErrTranslateStoreReadOnly
}
// If any values not found then recheck and then add under a write lock.
s.mu.Lock()
defer s.mu.Unlock()
// Recheck if value was created between the read lock and write lock.
idx := s.cols[index]
if idx != nil {
var writeRequired bool
for i := range values {
if ret[i] != 0 {
continue
}
v, ok := idx.idByKey([]byte(values[i]))
if !ok {
writeRequired = true
continue
}
ret[i] = v
}
if !writeRequired {
return ret, nil
}
}
// Create index map if it doesn't exists.
if idx == nil {
idx = newIndex(s.data)
s.cols[index] = idx
}
// Append new identifiers to log.
entry := &LogEntry{
Type: LogEntryTypeInsertColumn,
Index: []byte(index),
IDs: make([]uint64, 0, len(values)),
Keys: make([][]byte, 0, len(values)),
}
check := make(map[string]uint64)
for i := range values {
if ret[i] != 0 {
continue
}
v, found := check[values[i]]
if !found {
idx.seq++
v = idx.seq
check[values[i]] = v
}
ret[i] = v
entry.IDs = append(entry.IDs, v)
entry.Keys = append(entry.Keys, []byte(values[i]))
}
// Write entry.
if err := s.appendEntry(entry); err != nil {
return nil, err
}
return ret, nil
}
// TranslateColumnToString converts a uint64 id to its associated string value.
// If the id is not associated with a string value then a blank string is returned.
func (s *TranslateFile) TranslateColumnToString(index string, value uint64) (string, error) {
s.mu.RLock()
if idx := s.cols[index]; idx != nil {
if ret, ok := idx.keyByID(value); ok {
s.mu.RUnlock()
return string(ret), nil
}
}
s.mu.RUnlock()
return "", nil
}
// TranslateRowsToUint64 converts a slice of row keys to a slice of row IDs.
func (s *TranslateFile) TranslateRowsToUint64(index, field string, values []string) ([]uint64, error) {
key := fieldKey{index, field}
ret := make([]uint64, len(values))
// Read value under read lock.
s.mu.RLock()
if idx := s.rows[key]; idx != nil {
var writeRequired bool
for i := range values {
v, ok := idx.idByKey([]byte(values[i]))
if !ok {
writeRequired = true
}
ret[i] = v
}
if !writeRequired {
s.mu.RUnlock()
return ret, nil
}
}
s.mu.RUnlock()
// Return error if not all values could be translated and this store is read-only.
if s.isReadOnly() {
return ret, ErrTranslateStoreReadOnly
}
// If any values not found then recheck and then add under a write lock.
s.mu.Lock()
defer s.mu.Unlock()
// Recheck if value was created between the read lock and write lock.
idx := s.rows[key]
if idx != nil {
var writeRequired bool
for i := range values {
if ret[i] != 0 {
continue
}
v, ok := idx.idByKey([]byte(values[i]))
if !ok {
writeRequired = true
continue
}
ret[i] = v
}
if !writeRequired {
return ret, nil
}
}
// Create map if it doesn't exists.
if idx == nil {
idx = newIndex(s.data)
s.rows[key] = idx
}
// Append new identifiers to log.
entry := &LogEntry{
Type: LogEntryTypeInsertRow,
Index: []byte(index),
Field: []byte(field),
IDs: make([]uint64, 0, len(values)),
Keys: make([][]byte, 0, len(values)),
}
check := make(map[string]uint64)
for i := range values {
if ret[i] != 0 {
continue
}
v, found := check[values[i]]
if !found {
idx.seq++
v = idx.seq
check[values[i]] = v
}
ret[i] = v
entry.IDs = append(entry.IDs, v)
entry.Keys = append(entry.Keys, []byte(values[i]))
}
// Write entry.
if err := s.appendEntry(entry); err != nil {
return nil, err
}
return ret, nil
}
// TranslateRowToString translates a row ID to a string key.
func (s *TranslateFile) TranslateRowToString(index, field string, id uint64) (string, error) {
s.mu.RLock()
if idx := s.rows[fieldKey{index, field}]; idx != nil {
if ret, ok := idx.keyByID(id); ok {
s.mu.RUnlock()
return string(ret), nil
}
}
s.mu.RUnlock()
return "", nil
}
// Reader returns a reader that streams the underlying data file.
func (s *TranslateFile) Reader(ctx context.Context, offset int64) (io.ReadCloser, error) {
rc := newTranslateFileReader(ctx, s, offset)
if err := rc.Open(); err != nil {
return nil, err
}
return rc, nil
}
// LogEntry is a batch of Key/ID mappings which is replicated to other nodes
// for read-only key translation.
type LogEntry struct {
Type uint8
Index []byte
Field []byte
IDs []uint64
Keys [][]byte
// Length of the entry, in bytes.
// This is only populated after ReadFrom() or WriteTo().
Length uint64
}
// headerSize returns the number of bytes required for size, type, index, field, & pair count.
func (e *LogEntry) headerSize() int64 {
sz := uVarintSize(e.Length) + // total entry length
1 + // type
uVarintSize(uint64(len(e.Index))) + len(e.Index) + // Index length and data
uVarintSize(uint64(len(e.Field))) + len(e.Field) + // Field length and data
uVarintSize(uint64(len(e.IDs))) // ID/Key pair count
return int64(sz)
}
// ReadFrom deserializes a LogEntry from r. r must be a ByteReader.
func (e *LogEntry) ReadFrom(r io.Reader) (_ int64, err error) {
br := r.(io.ByteReader)
// Read the entry length.
if e.Length, err = binary.ReadUvarint(br); err != nil {
return int64(uVarintSize(e.Length)), err
}
// Read the entry type.
if err := binary.Read(r, binary.BigEndian, &e.Type); err != nil {
return 0, err
}
// Read index name.
if sz, err := binary.ReadUvarint(br); err != nil {
return 0, err
} else if sz == 0 {
e.Index = nil
} else {
e.Index = make([]byte, sz)
if _, err := io.ReadFull(r, e.Index); err != nil {
return 0, err
}
}
// Read field name.
if sz, err := binary.ReadUvarint(br); err != nil {
return 0, err
} else if sz == 0 {
e.Field = nil
} else {
e.Field = make([]byte, sz)
if _, err := io.ReadFull(r, e.Field); err != nil {
return 0, err
}
}
// Read key count.
if n, err := binary.ReadUvarint(br); err != nil {
return 0, err
} else if n == 0 {
e.IDs, e.Keys = nil, nil
} else {
e.IDs, e.Keys = make([]uint64, n), make([][]byte, n)
}
// Read each id/key pairs.
for i := range e.Keys {
// Read identifier.
if e.IDs[i], err = binary.ReadUvarint(br); err != nil {
return 0, err
}
// Read key.
if sz, err := binary.ReadUvarint(br); err != nil {
return 0, err
} else if sz > 0 {
e.Keys[i] = make([]byte, sz)
if _, err := io.ReadFull(r, e.Keys[i]); err != nil {
return 0, err
}
}
}
return int64(uVarintSize(e.Length)) + int64(e.Length), nil
}
// WriteTo serializes a LogEntry to w.
func (e *LogEntry) WriteTo(w io.Writer) (_ int64, err error) {
var buf bytes.Buffer
b := make([]byte, binary.MaxVarintLen64)
// Write the entry type.
if err := binary.Write(&buf, binary.BigEndian, e.Type); err != nil {
return 0, err
}
// Write the index name.
sz := binary.PutUvarint(b, uint64(len(e.Index)))
if _, err := buf.Write(b[:sz]); err != nil {
return 0, err
} else if _, err := buf.Write(e.Index); err != nil {
return 0, err
}
// Write field name.
sz = binary.PutUvarint(b, uint64(len(e.Field)))
if _, err := buf.Write(b[:sz]); err != nil {
return 0, err
} else if _, err := buf.Write(e.Field); err != nil {
return 0, err
}
// Write key count.
sz = binary.PutUvarint(b, uint64(len(e.IDs)))
if _, err := buf.Write(b[:sz]); err != nil {
return 0, err
}
// Write each id/key pairs.
for i := range e.Keys {
// Write identifier.
sz = binary.PutUvarint(b, e.IDs[i])
if _, err := buf.Write(b[:sz]); err != nil {
return 0, err
}
// Write key.
sz = binary.PutUvarint(b, uint64(len(e.Keys[i])))
if _, err := buf.Write(b[:sz]); err != nil {
return 0, err
} else if _, err := buf.Write(e.Keys[i]); err != nil {
return 0, err
}
}
// Write buffer size.
e.Length = uint64(buf.Len())
sz = binary.PutUvarint(b, e.Length)
if n, err := w.Write(b[:sz]); err != nil {
return int64(n), err
}
// Write buffer.
n, err := buf.WriteTo(w)
return int64(sz) + n, err
}
type fieldKey struct {
index string
field string
}
const defaultLoadFactor = 90
// index represents a two-way index between IDs and keys.
type index struct {
seq uint64 // autoincrement sequence
data []byte // memory-mapped file containing key data
// RHH hashmap for id-to-offset mapping.
// This is required so we don't need to store key data on the heap.
// https://cs.uwaterloo.ca/research/tr/1986/CS-86-14.pdf
elems []elem // id/offset key pairs
n uint64 // number of inuse elements
mask uint64 // mask applied for modulus
threshold uint64 // threshold when capacity doubles
loadFactor int // factor used to calculate threshold
// Builtin hashmap for offset-to-id mapping.
offsetsByID map[uint64]int64
}
func newIndex(data []byte) *index {
idx := &index{
data: data,
offsetsByID: make(map[uint64]int64),
loadFactor: defaultLoadFactor,
}
idx.alloc(pow2(uint64(256)))
return idx
}
// keyByID returns the key for a given ID, if it exists.
func (idx *index) keyByID(id uint64) ([]byte, bool) {
offset, ok := idx.offsetsByID[id]
if !ok {
return nil, false
}
return idx.lookupKey(offset), true
}
// idByKey returns the ID for a given key, if it exists.
func (idx *index) idByKey(key []byte) (uint64, bool) {
hash := hashKey(key)
pos := hash & idx.mask
var dist uint64
for {
if e := &idx.elems[pos]; e.hash == 0 {
return 0, false
} else if dist > idx.dist(e.hash, pos) {
return 0, false
} else if e.hash == hash && bytes.Equal(idx.lookupKey(e.offset), key) {
return e.id, true
}
pos = (pos + 1) & idx.mask
dist++
}
}
// insert adds the id/offset pair to the index.
// This function will resize the map if it crosses the threshold.
func (idx *index) insert(id uint64, offset int64) {
idx.n++
// Add to reverse lookup.
idx.offsetsByID[id] = offset
// Grow the map if we've run out of slots.
if idx.n > idx.threshold {
elems, capacity := idx.elems, uint64(len(idx.elems))
idx.alloc(uint64(len(idx.elems) * 2))
for i := uint64(0); i < capacity; i++ {
e := &elems[i]
if e.hash == 0 {
continue
}
idx.insertIDbyOffset(e.offset, e.id)
}
}
// If the key was overwritten then decrement the size.
if overwritten := idx.insertIDbyOffset(offset, id); overwritten {
idx.n--
}
}
// insertIDbyOffset writes to the RHH id-by-offset map.
func (idx *index) insertIDbyOffset(offset int64, id uint64) (overwritten bool) {
key := idx.lookupKey(offset)
hash := hashKey(key)
pos := hash & idx.mask
var dist uint64
for {
e := &idx.elems[pos]
// Exit if a matching or empty slot exists.
if e.hash == 0 {
e.hash, e.offset, e.id = hash, offset, id
return false
} else if bytes.Equal(idx.lookupKey(e.offset), key) {
e.hash, e.offset, e.id = hash, offset, id
return true
}
// Swap if current element has a lower probe distance.
d := idx.dist(e.hash, pos)
if d < dist {
hash, e.hash = e.hash, hash
offset, e.offset = e.offset, offset
id, e.id = e.id, id
dist = d
}
// Move position forward.
pos = (pos + 1) & idx.mask
dist++
}
}
// lookupKey returns the key at the given offset in the memory-mapped file.
func (idx *index) lookupKey(offset int64) []byte {
data := idx.data[offset:]
n, sz := binary.Uvarint(data)
if sz == 0 {
return nil
}
return data[sz : sz+int(n)]
}
func (idx *index) alloc(capacity uint64) {
idx.elems = make([]elem, capacity)
idx.threshold = (capacity * uint64(idx.loadFactor)) / 100
idx.mask = capacity - 1
}
func (idx *index) dist(hash, i uint64) uint64 {
return (i + uint64(len(idx.elems)) - (hash & idx.mask)) & idx.mask
}
type elem struct {
offset int64
id uint64
hash uint64
}
func hashKey(key []byte) uint64 {
h := xxhash.Sum64(key)
if h == 0 {
h = 1
}
return h
}
func pow2(v uint64) uint64 { // nolint: unparam
for i := uint64(2); i < 1<<62; i *= 2 {
if i >= v {
return i
}
}
panic("unreachable")
}
// translateFileReader implements a reader that continuously streams data from a store.
type translateFileReader struct {
ctx context.Context
store *TranslateFile
file *os.File
offset int64
notify <-chan struct{}
once sync.Once
closing chan struct{}
}
// newTranslateFileReader returns a new instance of translateFileReader.
func newTranslateFileReader(ctx context.Context, store *TranslateFile, offset int64) *translateFileReader {
return &translateFileReader{
ctx: ctx,
store: store,
offset: offset,
notify: store.WriteNotify(),
closing: make(chan struct{}),
}
}
// Open initializes the reader.
func (r *translateFileReader) Open() (err error) {
r.file, err = os.Open(r.store.Path)
return err
}
// Close closes the underlying file reader.
func (r *translateFileReader) Close() error {
r.once.Do(func() { close(r.closing) })
if r.file != nil {
return r.file.Close()
}
return nil
}
// Read reads the next section of the available data to p. This should always
// read from the start of an entry and read n bytes to the end of another entry.
func (r *translateFileReader) Read(p []byte) (n int, err error) {
for {
// Obtain notification channel before we check for new data.
notify := r.store.WriteNotify()
// Exit if we can read one or more valid entries or we receive an error.
if n, err = r.read(p); n > 0 || err != nil {
return n, err
}
// Wait for new data or close.
select {
case <-r.ctx.Done():
return 0, r.ctx.Err()
case <-r.closing:
return 0, ErrTranslateStoreReaderClosed
case <-r.store.Closing():
return 0, ErrTranslateStoreClosed
case <-notify:
continue
}
}
}
// read reads up to len(p) bytes into p.
func (r *translateFileReader) read(p []byte) (n int, err error) {
sz := r.store.size()
// Exit if there is no new data.
if sz < r.offset {
return 0, fmt.Errorf("pilosa: translate store reader past file size: sz=%d off=%d", sz, r.offset)
} else if sz == r.offset {
return 0, nil
}
if max := sz - r.offset; int64(len(p)) > max {
// Shorten buffer to maximum read size.
p = p[:max]
}
// Read data from file at offset.
n, err = r.file.ReadAt(p, r.offset)
r.offset += int64(n)
return n, err
}
// Copied & modified from encoding/binary.
func uVarintSize(x uint64) (i int) {
for x >= 0x80 {
x >>= 7
i++
}
return i + 1
}
// nopTStore represents a TranslateStore that doesn't do anything.
var nopTStore TranslateStore = nopTranslateStore{}
// newNopTranslateStore returns a translate store which does nothing. It returns a global
// object to avoid unnecessary allocations.
func newNopTranslateStore(interface{}) TranslateStore { return nopTStore }
// nopTranslateStore represents a no-op implementation of the TranslateStore interface.
type nopTranslateStore struct{}
// TranslateColumnsToUint64 is a no-op implementation of the TranslateStore TranslateColumnsToUint64 method.
func (s nopTranslateStore) TranslateColumnsToUint64(index string, values []string) ([]uint64, error) {
return []uint64{}, nil
}
// TranslateColumnToString is a no-op implementation of the TranslateStore TranslateColumnToString method.
func (s nopTranslateStore) TranslateColumnToString(index string, values uint64) (string, error) {
return "", nil
}
// TranslateRowsToUint64 is a no-op implementation of the TranslateStore TranslateRowsToUint64 method.
func (s nopTranslateStore) TranslateRowsToUint64(index, field string, values []string) ([]uint64, error) {
return []uint64{}, nil
}
// TranslateRowToString is a no-op implementation of the TranslateStore TranslateRowToString method.
func (s nopTranslateStore) TranslateRowToString(index, field string, values uint64) (string, error) {
return "", nil
}
// Reader is a no-op implementation of the TranslateStore Reader method.
func (s nopTranslateStore) Reader(ctx context.Context, off int64) (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(nil)), nil
}