742 lines
19 KiB
Go
742 lines
19 KiB
Go
// Copyright (C) 2025 wangyusong
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package chunk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/edsrzf/mmap-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
|
|
"github.com/glidea/zenfeed/pkg/component"
|
|
"github.com/glidea/zenfeed/pkg/model"
|
|
"github.com/glidea/zenfeed/pkg/telemetry"
|
|
telemetrymodel "github.com/glidea/zenfeed/pkg/telemetry/model"
|
|
"github.com/glidea/zenfeed/pkg/util/buffer"
|
|
timeutil "github.com/glidea/zenfeed/pkg/util/time"
|
|
)
|
|
|
|
// --- Interface code block ---
|
|
|
|
// File is the interface for a chunk file.
|
|
// Concurrent safe.
|
|
type File interface {
|
|
component.Component
|
|
|
|
// EnsureReadonly ensures the file is readonly (can not Append).
|
|
// It should be fast when the file already is readonly.
|
|
// It will ensure the writeonly related resources are closed,
|
|
// and open the readonly related resources, such as mmap to save memory.
|
|
EnsureReadonly(ctx context.Context) (err error)
|
|
Count(ctx context.Context) (count uint32)
|
|
|
|
// Append appends feeds to the file.
|
|
// onSuccess is called when the feed is appended successfully (synchronously).
|
|
// The offset is the offset of the feed in the file.
|
|
// !!! It doesn't buffer the data between requests, so the caller should buffer the feeds to avoid high I/O.
|
|
Append(ctx context.Context, feeds []*Feed, onSuccess func(feed *Feed, offset uint64) error) (err error)
|
|
|
|
// Read reads a feed from the file.
|
|
Read(ctx context.Context, offset uint64) (feed *Feed, err error)
|
|
|
|
// Range ranges over all feeds in the file.
|
|
Range(ctx context.Context, iter func(feed *Feed, offset uint64) (err error)) (err error)
|
|
}
|
|
|
|
// Config for a chunk file.
|
|
type Config struct {
|
|
// Path is the path to the chunk file.
|
|
// If the file does not exist, it will be created.
|
|
// If the file exists, it will be reloaded.
|
|
Path string
|
|
// ReadonlyAtFirst indicates whether the file should be readonly at first.
|
|
// If file of path does not exist, it cannot be true.
|
|
ReadonlyAtFirst bool
|
|
}
|
|
|
|
func (c *Config) Validate() (fileExists bool, err error) {
|
|
if c.Path == "" {
|
|
return false, errors.New("path is required")
|
|
}
|
|
|
|
fi, err := os.Stat(c.Path)
|
|
switch {
|
|
case err == nil:
|
|
if fi.IsDir() {
|
|
return false, errors.New("path is a directory")
|
|
}
|
|
|
|
return true, nil
|
|
|
|
case os.IsNotExist(err):
|
|
if c.ReadonlyAtFirst {
|
|
return false, errors.New("path does not exist")
|
|
}
|
|
|
|
return false, nil
|
|
|
|
default:
|
|
return false, errors.Wrap(err, "stat path")
|
|
}
|
|
}
|
|
|
|
type Dependencies struct{}
|
|
|
|
// File struct.
|
|
var (
|
|
headerBytes = 64
|
|
headerMagicNumber = []byte{0x77, 0x79, 0x73, 0x20, 0x69, 0x73, 0x20,
|
|
0x61, 0x77, 0x65, 0x73, 0x6f, 0x6d, 0x65, 0x00, 0x00}
|
|
headerMagicNumberBytes = 16
|
|
headerVersionStart = headerMagicNumberBytes
|
|
headerVersion = uint32(1)
|
|
headerVersionBytes = 4
|
|
dataStart = headerBytes
|
|
|
|
header = func() []byte {
|
|
b := make([]byte, headerBytes)
|
|
copy(b[:headerMagicNumberBytes], headerMagicNumber)
|
|
binary.LittleEndian.PutUint32(b[headerVersionStart:headerVersionStart+headerVersionBytes], headerVersion)
|
|
|
|
return b
|
|
}()
|
|
)
|
|
|
|
// Metrics.
|
|
var (
|
|
modes = []string{"readwrite", "readonly"}
|
|
feedCount = promauto.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Namespace: model.AppName,
|
|
Subsystem: "chunk",
|
|
Name: "feed_count",
|
|
Help: "Number of feeds in the chunk file.",
|
|
},
|
|
[]string{telemetrymodel.KeyComponent, telemetrymodel.KeyComponentInstance, "mode"},
|
|
)
|
|
byteSize = promauto.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Namespace: model.AppName,
|
|
Subsystem: "chunk",
|
|
Name: "bytes",
|
|
Help: "Size of the chunk file.",
|
|
},
|
|
[]string{telemetrymodel.KeyComponent, telemetrymodel.KeyComponentInstance, "mode"},
|
|
)
|
|
)
|
|
|
|
// --- Factory code block ---
|
|
type Factory component.Factory[File, Config, Dependencies]
|
|
|
|
func NewFactory(mockOn ...component.MockOption) Factory {
|
|
if len(mockOn) > 0 {
|
|
return component.FactoryFunc[File, Config, Dependencies](
|
|
func(instance string, config *Config, dependencies Dependencies) (File, error) {
|
|
m := &mockFile{}
|
|
component.MockOptions(mockOn).Apply(&m.Mock)
|
|
|
|
return m, nil
|
|
},
|
|
)
|
|
}
|
|
|
|
return component.FactoryFunc[File, Config, Dependencies](new)
|
|
}
|
|
|
|
// new creates a new chunk file.
|
|
// It will create a new chunk file if the file that path points to does not exist.
|
|
// It will open the file if the file exists, and reload it.
|
|
// If readonlyAtFirst is true, it will open the file readonly.
|
|
func new(instance string, config *Config, dependencies Dependencies) (File, error) {
|
|
fileExists, err := config.Validate()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "validate config")
|
|
}
|
|
|
|
osFile, readWriteBuf, appendOffset, readonlyMmap, count, err := init0(fileExists, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rn atomic.Bool
|
|
rn.Store(config.ReadonlyAtFirst)
|
|
var cnt atomic.Uint32
|
|
cnt.Store(count)
|
|
|
|
return &file{
|
|
Base: component.New(&component.BaseConfig[Config, Dependencies]{
|
|
Name: "FeedChunk",
|
|
Instance: instance,
|
|
Config: config,
|
|
Dependencies: dependencies,
|
|
}),
|
|
f: osFile,
|
|
readWriteBuf: readWriteBuf,
|
|
appendOffset: appendOffset,
|
|
readonlyMmap: readonlyMmap,
|
|
readonly: &rn,
|
|
count: &cnt,
|
|
}, nil
|
|
}
|
|
|
|
func init0(
|
|
fileExists bool,
|
|
config *Config,
|
|
) (
|
|
osFile *os.File,
|
|
readWriteBuf *buffer.Bytes,
|
|
appendOffset uint64,
|
|
readonlyMmap mmap.MMap,
|
|
count uint32,
|
|
err error,
|
|
) {
|
|
// Ensure file.
|
|
if fileExists {
|
|
osFile, err = loadFromExisting(config.Path, config.ReadonlyAtFirst)
|
|
if err != nil {
|
|
return nil, nil, 0, nil, 0, errors.Wrap(err, "load from existing")
|
|
}
|
|
|
|
} else { // Create new file.
|
|
if config.ReadonlyAtFirst {
|
|
return nil, nil, 0, nil, 0, errors.New("cannot create readonly file")
|
|
}
|
|
|
|
osFile, err = createNewOSFile(config.Path)
|
|
if err != nil {
|
|
return nil, nil, 0, nil, 0, errors.Wrap(err, "create new os file")
|
|
}
|
|
}
|
|
|
|
// Setup for Read.
|
|
readWriteBuf, count, err = validateOSFile(osFile)
|
|
if err != nil {
|
|
_ = osFile.Close()
|
|
|
|
return nil, nil, 0, nil, 0, errors.Wrap(err, "validate os file")
|
|
}
|
|
|
|
if config.ReadonlyAtFirst {
|
|
readWriteBuf = nil // Help GC.
|
|
|
|
m, err := mmap.Map(osFile, mmap.RDONLY, 0)
|
|
if err != nil {
|
|
_ = osFile.Close()
|
|
|
|
return nil, nil, 0, nil, 0, errors.Wrap(err, "mmap file")
|
|
}
|
|
|
|
readonlyMmap = m
|
|
|
|
} else {
|
|
appendOffset = uint64(readWriteBuf.Len())
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func validateOSFile(f *os.File) (readWriteBuf *buffer.Bytes, count uint32, err error) {
|
|
header, err := validateHeader(f)
|
|
if err != nil {
|
|
return nil, 0, errors.Wrap(err, "validate header")
|
|
}
|
|
readWriteBuf = &buffer.Bytes{B: header} // len(header) == cap(header).
|
|
|
|
if _, err := f.Seek(int64(dataStart), io.SeekStart); err != nil {
|
|
return nil, 0, errors.Wrap(err, "seek to data start")
|
|
}
|
|
tr := &trackReader{Reader: f}
|
|
var lastSuccessReaded int
|
|
|
|
var p Feed
|
|
for {
|
|
err := p.validateFrom(tr, readWriteBuf)
|
|
switch {
|
|
case err == nil:
|
|
count++
|
|
lastSuccessReaded = tr.Readed()
|
|
|
|
continue
|
|
|
|
case (errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) ||
|
|
errors.Is(err, errChecksumMismatch):
|
|
|
|
// Truncate uncompleted feed if any.
|
|
readWriteBuf.B = readWriteBuf.B[:lastSuccessReaded+len(header)]
|
|
|
|
return readWriteBuf, count, nil
|
|
|
|
default:
|
|
return nil, 0, errors.Wrap(err, "validate payload")
|
|
}
|
|
}
|
|
}
|
|
|
|
func validateHeader(f *os.File) (header []byte, err error) {
|
|
header = make([]byte, headerBytes)
|
|
if _, err := f.ReadAt(header, 0); err != nil {
|
|
return nil, errors.Wrap(err, "read header")
|
|
}
|
|
|
|
// Validate magic number.
|
|
if !bytes.Equal(header[:headerMagicNumberBytes], headerMagicNumber) {
|
|
return nil, errors.New("invalid magic number")
|
|
}
|
|
|
|
// Validate version.
|
|
version := binary.LittleEndian.Uint32(header[headerVersionStart : headerVersionStart+headerVersionBytes])
|
|
if version != headerVersion {
|
|
return nil, errors.New("invalid version")
|
|
}
|
|
|
|
return header, nil
|
|
}
|
|
|
|
func loadFromExisting(path string, readonlyAtFirst bool) (osFile *os.File, err error) {
|
|
flag := os.O_RDWR
|
|
if readonlyAtFirst {
|
|
flag = os.O_RDONLY
|
|
}
|
|
|
|
osFile, err = os.OpenFile(path, flag, 0600)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "open file")
|
|
}
|
|
|
|
return osFile, nil
|
|
}
|
|
|
|
func createNewOSFile(path string) (osFile *os.File, err error) {
|
|
osFile, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "create file")
|
|
}
|
|
|
|
if _, err = osFile.Write(header); err != nil {
|
|
_ = osFile.Close()
|
|
|
|
return nil, errors.Wrap(err, "write header")
|
|
}
|
|
|
|
if err = osFile.Sync(); err != nil {
|
|
_ = osFile.Close()
|
|
|
|
return nil, errors.Wrap(err, "sync file")
|
|
}
|
|
|
|
return osFile, nil
|
|
}
|
|
|
|
// --- Implementation code block ---
|
|
type file struct {
|
|
*component.Base[Config, Dependencies]
|
|
|
|
f *os.File
|
|
count *atomic.Uint32
|
|
readonly *atomic.Bool
|
|
|
|
mu sync.RWMutex
|
|
|
|
// Only readwrite.
|
|
readWriteBuf *buffer.Bytes
|
|
appendOffset uint64
|
|
|
|
// Only readonly.
|
|
readonlyMmap mmap.MMap
|
|
}
|
|
|
|
func (f *file) Run() error {
|
|
f.MarkReady()
|
|
|
|
return timeutil.Tick(f.Context(), 30*time.Second, func() error {
|
|
mode := "readwrite"
|
|
sizeValue := f.appendOffset
|
|
if f.readonly.Load() {
|
|
mode = "readonly"
|
|
sizeValue = uint64(len(f.readonlyMmap))
|
|
}
|
|
|
|
feedCount.WithLabelValues(append(f.TelemetryLabelsIDFields(), mode)...).Set(float64(f.Count(context.Background())))
|
|
byteSize.WithLabelValues(append(f.TelemetryLabelsIDFields(), mode)...).Set(float64(sizeValue))
|
|
for _, m := range modes {
|
|
if m == mode {
|
|
continue
|
|
}
|
|
feedCount.DeleteLabelValues(append(f.TelemetryLabelsIDFields(), m)...)
|
|
byteSize.DeleteLabelValues(append(f.TelemetryLabelsIDFields(), m)...)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (f *file) Close() error {
|
|
// Close Run().
|
|
if err := f.Base.Close(); err != nil {
|
|
return errors.Wrap(err, "closing base")
|
|
}
|
|
|
|
// Clean metrics.
|
|
feedCount.DeletePartialMatch(f.TelemetryLabelsID())
|
|
byteSize.DeletePartialMatch(f.TelemetryLabelsID())
|
|
|
|
// Unmap if readonly.
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.readonlyMmap != nil {
|
|
if err := f.readonlyMmap.Unmap(); err != nil {
|
|
return errors.Wrap(err, "unmap file")
|
|
}
|
|
f.readonlyMmap = nil
|
|
}
|
|
|
|
// Close file.
|
|
if err := f.f.Close(); err != nil {
|
|
return errors.Wrap(err, "close file")
|
|
}
|
|
f.f = nil
|
|
f.appendOffset = 0
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *file) EnsureReadonly(ctx context.Context) (err error) {
|
|
ctx = telemetry.StartWith(ctx, append(f.TelemetryLabels(), telemetrymodel.KeyOperation, "EnsureReadonly")...)
|
|
defer func() { telemetry.End(ctx, err) }()
|
|
|
|
// Fast path - already readonly.
|
|
if f.readonly.Load() {
|
|
return nil
|
|
}
|
|
|
|
// Acquire write lock
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.readonly.Load() {
|
|
return nil
|
|
}
|
|
|
|
// Clear readwrite resources.
|
|
f.readWriteBuf = nil
|
|
|
|
// Open mmap.
|
|
m, err := mmap.Map(f.f, mmap.RDONLY, 0)
|
|
if err != nil {
|
|
return errors.Wrap(err, "mmap file")
|
|
}
|
|
|
|
// Update state.
|
|
f.readonlyMmap = m
|
|
f.readonly.Store(true)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *file) Count(ctx context.Context) uint32 {
|
|
ctx = telemetry.StartWith(ctx, append(f.TelemetryLabels(), telemetrymodel.KeyOperation, "Count")...)
|
|
defer func() { telemetry.End(ctx, nil) }()
|
|
|
|
return f.count.Load()
|
|
}
|
|
|
|
func (f *file) Append(ctx context.Context, feeds []*Feed, onSuccess func(feed *Feed, offset uint64) error) (err error) {
|
|
ctx = telemetry.StartWith(ctx, append(f.TelemetryLabels(), telemetrymodel.KeyOperation, "Append")...)
|
|
defer func() { telemetry.End(ctx, err) }()
|
|
|
|
f.mu.Lock()
|
|
|
|
// Precheck.
|
|
if f.readonly.Load() {
|
|
f.mu.Unlock()
|
|
|
|
return errors.New("file is readonly")
|
|
}
|
|
|
|
// Encode feeds into buffer.
|
|
currentAppendOffset := f.appendOffset
|
|
relativeOffsets, encodedBytesCount, err := f.encodeFeeds(feeds)
|
|
if err != nil {
|
|
f.readWriteBuf.B = f.readWriteBuf.B[:currentAppendOffset]
|
|
f.mu.Unlock()
|
|
|
|
return errors.Wrap(err, "encode feeds")
|
|
}
|
|
|
|
// Prepare for commit.
|
|
encodedData := f.readWriteBuf.Bytes()[currentAppendOffset:]
|
|
newAppendOffset := currentAppendOffset + uint64(encodedBytesCount)
|
|
|
|
// Commit data and header to file.
|
|
if err = f.commitAppendToFile(encodedData, currentAppendOffset); err != nil {
|
|
f.readWriteBuf.B = f.readWriteBuf.B[:currentAppendOffset]
|
|
f.mu.Unlock()
|
|
|
|
return errors.Wrap(err, "commit append to file")
|
|
}
|
|
|
|
// Update internal state on successful commit.
|
|
f.appendOffset = newAppendOffset
|
|
f.count.Add(uint32(len(feeds)))
|
|
f.mu.Unlock()
|
|
|
|
// Call callbacks after releasing the lock.
|
|
absoluteOffsets := make([]uint64, len(relativeOffsets))
|
|
for i, relOff := range relativeOffsets {
|
|
absoluteOffsets[i] = currentAppendOffset + relOff // Calculate absolute offsets based on append position.
|
|
}
|
|
if err := f.notifySuccess(feeds, absoluteOffsets, onSuccess); err != nil {
|
|
return errors.Wrap(err, "notify success callbacks")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *file) Read(ctx context.Context, offset uint64) (feed *Feed, err error) {
|
|
ctx = telemetry.StartWith(ctx, append(f.TelemetryLabels(), telemetrymodel.KeyOperation, "Read")...)
|
|
defer func() { telemetry.End(ctx, err) }()
|
|
|
|
// Validate offset.
|
|
if offset < uint64(dataStart) {
|
|
return nil, errors.New("offset too small")
|
|
}
|
|
|
|
// Handle readonly mode.
|
|
if f.readonly.Load() {
|
|
if offset >= uint64(len(f.readonlyMmap)) {
|
|
return nil, errors.New("offset too large")
|
|
}
|
|
feed, _, err = f.readFeed(ctx, f.readonlyMmap, offset)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "read feed")
|
|
}
|
|
|
|
return feed, nil
|
|
}
|
|
|
|
// Handle readwrite mode.
|
|
f.mu.RLock()
|
|
defer f.mu.RUnlock()
|
|
if offset >= f.appendOffset {
|
|
return nil, errors.New("offset too large")
|
|
}
|
|
|
|
feed, _, err = f.readFeed(ctx, f.readWriteBuf.Bytes(), offset)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "read feed")
|
|
}
|
|
|
|
return feed, nil
|
|
}
|
|
|
|
func (f *file) Range(ctx context.Context, iter func(feed *Feed, offset uint64) error) (err error) {
|
|
ctx = telemetry.StartWith(ctx, append(f.TelemetryLabels(), telemetrymodel.KeyOperation, "Range")...)
|
|
defer func() { telemetry.End(ctx, err) }()
|
|
|
|
// Handle readonly mode.
|
|
if f.readonly.Load() {
|
|
// Start from data section.
|
|
offset := uint64(dataStart)
|
|
for offset < uint64(len(f.readonlyMmap)) {
|
|
feed, n, err := f.readFeed(ctx, f.readonlyMmap, offset)
|
|
if err != nil {
|
|
return errors.Wrap(err, "read feed")
|
|
}
|
|
if err := iter(feed, offset); err != nil {
|
|
return errors.Wrap(err, "iterate feed")
|
|
}
|
|
|
|
// Move to next feed.
|
|
offset += uint64(n) // G115: Safe conversion as n is uint32
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Handle readwrite mode.
|
|
f.mu.RLock()
|
|
defer f.mu.RUnlock()
|
|
data := f.readWriteBuf.Bytes()
|
|
offset := uint64(dataStart)
|
|
for offset < f.appendOffset { // appendOffset is already checked/maintained correctly.
|
|
feed, n, err := f.readFeed(ctx, data, offset)
|
|
if err != nil {
|
|
return errors.Wrap(err, "read feed")
|
|
}
|
|
if err := iter(feed, offset); err != nil {
|
|
return errors.Wrap(err, "iterate feed")
|
|
}
|
|
|
|
// Move to next feed.
|
|
offset += uint64(n)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const estimatedFeedSize = 4 * 1024
|
|
|
|
// encodeFeeds encodes a slice of feeds into the internal readWriteBuf.
|
|
// It returns the relative offsets of each feed within the newly added data,
|
|
// the total number of bytes encoded, and any error encountered.
|
|
func (f *file) encodeFeeds(feeds []*Feed) (relativeOffsets []uint64, encodedBytesCount int, err error) {
|
|
relativeOffsets = make([]uint64, len(feeds))
|
|
startOffset := f.readWriteBuf.Len()
|
|
|
|
f.readWriteBuf.EnsureRemaining(estimatedFeedSize * len(feeds))
|
|
|
|
for i, feed := range feeds {
|
|
currentOffsetInBuf := f.readWriteBuf.Len()
|
|
relativeOffsets[i] = uint64(currentOffsetInBuf - startOffset)
|
|
if err := feed.encodeTo(f.readWriteBuf); err != nil {
|
|
return nil, 0, errors.Wrapf(err, "encode feed %d", i)
|
|
}
|
|
}
|
|
|
|
encodedBytesCount = f.readWriteBuf.Len() - startOffset
|
|
|
|
return relativeOffsets, encodedBytesCount, nil
|
|
}
|
|
|
|
// commitAppendToFile writes the encoded data and updated header to the file and syncs.
|
|
func (f *file) commitAppendToFile(data []byte, currentAppendOffset uint64) error {
|
|
// Append data.
|
|
if _, err := f.f.WriteAt(data, int64(currentAppendOffset)); err != nil {
|
|
// Data might be partially written.
|
|
// We will overwrite it in the next append.
|
|
return errors.Wrap(err, "write feeds")
|
|
}
|
|
|
|
// Sync file to persist changes.
|
|
if err := f.f.Sync(); err != nil {
|
|
return errors.Wrap(err, "sync file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// notifySuccess calls the onSuccess callback for each successfully appended feed.
|
|
func (f *file) notifySuccess(
|
|
feeds []*Feed,
|
|
absoluteOffsets []uint64,
|
|
onSuccess func(feed *Feed, offset uint64) error,
|
|
) error {
|
|
if onSuccess == nil {
|
|
return nil
|
|
}
|
|
for i, feed := range feeds {
|
|
if err := onSuccess(feed, absoluteOffsets[i]); err != nil {
|
|
// Return the first error encountered during callbacks.
|
|
return errors.Wrapf(err, "on success callback for feed %d", i)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *file) readFeed(ctx context.Context, data []byte, offset uint64) (feed *Feed, length int, err error) {
|
|
ctx = telemetry.StartWith(ctx, append(f.TelemetryLabels(), telemetrymodel.KeyOperation, "readFeed")...)
|
|
defer func() { telemetry.End(ctx, err) }()
|
|
|
|
// Prepare reader.
|
|
r := io.NewSectionReader(bytes.NewReader(data), int64(offset), int64(uint64(len(data))-offset))
|
|
tr := &trackReader{Reader: r}
|
|
|
|
// Decode feed.
|
|
feed = &Feed{Feed: &model.Feed{}}
|
|
if err = feed.decodeFrom(tr); err != nil {
|
|
return nil, 0, errors.Wrap(err, "decode feed")
|
|
}
|
|
|
|
return feed, tr.Readed(), nil
|
|
}
|
|
|
|
type trackReader struct {
|
|
io.Reader
|
|
length int
|
|
}
|
|
|
|
func (r *trackReader) Read(p []byte) (n int, err error) {
|
|
n, err = r.Reader.Read(p)
|
|
r.length += n
|
|
|
|
return
|
|
}
|
|
|
|
func (r *trackReader) Readed() int {
|
|
return r.length
|
|
}
|
|
|
|
type mockFile struct {
|
|
component.Mock
|
|
}
|
|
|
|
func (m *mockFile) Run() error {
|
|
args := m.Called()
|
|
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *mockFile) Ready() <-chan struct{} {
|
|
args := m.Called()
|
|
|
|
return args.Get(0).(<-chan struct{})
|
|
}
|
|
|
|
func (m *mockFile) Close() error {
|
|
args := m.Called()
|
|
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *mockFile) Append(ctx context.Context, feeds []*Feed, onSuccess func(feed *Feed, offset uint64) error) error {
|
|
args := m.Called(ctx, feeds, onSuccess)
|
|
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *mockFile) Read(ctx context.Context, offset uint64) (*Feed, error) {
|
|
args := m.Called(ctx, offset)
|
|
|
|
return args.Get(0).(*Feed), args.Error(1)
|
|
}
|
|
|
|
func (m *mockFile) Range(ctx context.Context, iter func(feed *Feed, offset uint64) error) error {
|
|
args := m.Called(ctx, iter)
|
|
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *mockFile) Count(ctx context.Context) uint32 {
|
|
args := m.Called(ctx)
|
|
|
|
return args.Get(0).(uint32)
|
|
}
|
|
|
|
func (m *mockFile) EnsureReadonly(ctx context.Context) error {
|
|
args := m.Called(ctx)
|
|
|
|
return args.Error(0)
|
|
}
|