// 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 }