286 lines
7.4 KiB
Go
286 lines
7.4 KiB
Go
package primary
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/glidea/zenfeed/pkg/component"
|
|
"github.com/glidea/zenfeed/pkg/storage/feed/block/index"
|
|
telemetry "github.com/glidea/zenfeed/pkg/telemetry"
|
|
telemetrymodel "github.com/glidea/zenfeed/pkg/telemetry/model"
|
|
)
|
|
|
|
// --- Interface code block ---
|
|
type Index interface {
|
|
component.Component
|
|
index.Codec
|
|
|
|
// Search returns item location by ID.
|
|
Search(ctx context.Context, id uint64) (ref FeedRef, ok bool)
|
|
// Add adds item location to the index.
|
|
Add(ctx context.Context, id uint64, item FeedRef)
|
|
// IDs returns all item IDs.
|
|
IDs(ctx context.Context) (ids map[uint64]bool)
|
|
// Count returns the number of feeds in the index.
|
|
Count(ctx context.Context) (count uint32)
|
|
}
|
|
|
|
type Config struct{}
|
|
|
|
type Dependencies struct{}
|
|
|
|
var (
|
|
headerMagicNumber = []byte{0x77, 0x79, 0x73, 0x20, 0x69, 0x73, 0x20,
|
|
0x61, 0x77, 0x65, 0x73, 0x6f, 0x6d, 0x65, 0x00, 0x00}
|
|
headerVersion = uint8(1)
|
|
)
|
|
|
|
type FeedRef struct {
|
|
Chunk uint32
|
|
Offset uint64
|
|
Time time.Time
|
|
}
|
|
|
|
// --- Factory code block ---
|
|
type Factory component.Factory[Index, Config, Dependencies]
|
|
|
|
func NewFactory(mockOn ...component.MockOption) Factory {
|
|
if len(mockOn) > 0 {
|
|
return component.FactoryFunc[Index, Config, Dependencies](
|
|
func(instance string, config *Config, dependencies Dependencies) (Index, error) {
|
|
m := &mockIndex{}
|
|
component.MockOptions(mockOn).Apply(&m.Mock)
|
|
|
|
return m, nil
|
|
},
|
|
)
|
|
}
|
|
|
|
return component.FactoryFunc[Index, Config, Dependencies](new)
|
|
}
|
|
|
|
func new(instance string, config *Config, dependencies Dependencies) (Index, error) {
|
|
return &idx{
|
|
Base: component.New(&component.BaseConfig[Config, Dependencies]{
|
|
Name: "FeedPrimaryIndex",
|
|
Instance: instance,
|
|
Config: config,
|
|
Dependencies: dependencies,
|
|
}),
|
|
m: make(map[uint64]FeedRef, 64),
|
|
}, nil
|
|
}
|
|
|
|
// --- Implementation code block ---
|
|
type idx struct {
|
|
*component.Base[Config, Dependencies]
|
|
|
|
m map[uint64]FeedRef
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (idx *idx) Search(ctx context.Context, id uint64) (ref FeedRef, ok bool) {
|
|
ctx = telemetry.StartWith(ctx, append(idx.TelemetryLabels(), telemetrymodel.KeyOperation, "Search")...)
|
|
defer func() { telemetry.End(ctx, nil) }()
|
|
|
|
idx.mu.RLock()
|
|
defer idx.mu.RUnlock()
|
|
ref, ok = idx.m[id]
|
|
|
|
return ref, ok
|
|
}
|
|
|
|
func (idx *idx) Add(ctx context.Context, id uint64, item FeedRef) {
|
|
ctx = telemetry.StartWith(ctx, append(idx.TelemetryLabels(), telemetrymodel.KeyOperation, "Add")...)
|
|
defer func() { telemetry.End(ctx, nil) }()
|
|
|
|
idx.mu.Lock()
|
|
defer idx.mu.Unlock()
|
|
item.Time = item.Time.In(time.UTC)
|
|
idx.m[id] = item
|
|
}
|
|
|
|
func (idx *idx) IDs(ctx context.Context) (ids map[uint64]bool) {
|
|
ctx = telemetry.StartWith(ctx, append(idx.TelemetryLabels(), telemetrymodel.KeyOperation, "IDs")...)
|
|
defer func() { telemetry.End(ctx, nil) }()
|
|
|
|
idx.mu.RLock()
|
|
defer idx.mu.RUnlock()
|
|
result := make(map[uint64]bool, len(idx.m))
|
|
for id := range idx.m {
|
|
result[id] = true
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (idx *idx) Count(ctx context.Context) (count uint32) {
|
|
ctx = telemetry.StartWith(ctx, append(idx.TelemetryLabels(), telemetrymodel.KeyOperation, "Count")...)
|
|
defer func() { telemetry.End(ctx, nil) }()
|
|
|
|
idx.mu.RLock()
|
|
defer idx.mu.RUnlock()
|
|
|
|
return uint32(len(idx.m))
|
|
}
|
|
|
|
func (idx *idx) EncodeTo(ctx context.Context, w io.Writer) (err error) {
|
|
ctx = telemetry.StartWith(ctx, append(idx.TelemetryLabels(), telemetrymodel.KeyOperation, "EncodeTo")...)
|
|
defer func() { telemetry.End(ctx, err) }()
|
|
idx.mu.RLock()
|
|
defer idx.mu.RUnlock()
|
|
|
|
// Write header.
|
|
if _, err := w.Write(headerMagicNumber); err != nil {
|
|
return errors.Wrap(err, "write header magic number")
|
|
}
|
|
if _, err := w.Write([]byte{headerVersion}); err != nil {
|
|
return errors.Wrap(err, "write header version")
|
|
}
|
|
|
|
// Write map count.
|
|
count := uint64(len(idx.m))
|
|
if err := binary.Write(w, binary.LittleEndian, count); err != nil {
|
|
return errors.Wrap(err, "write map count")
|
|
}
|
|
|
|
// Write all key-value pairs.
|
|
for id, ref := range idx.m {
|
|
// Write Key.
|
|
if err := binary.Write(w, binary.LittleEndian, id); err != nil {
|
|
return errors.Wrap(err, "write id")
|
|
}
|
|
|
|
// Write Value.
|
|
if err := binary.Write(w, binary.LittleEndian, ref.Chunk); err != nil {
|
|
return errors.Wrap(err, "write chunk")
|
|
}
|
|
if err := binary.Write(w, binary.LittleEndian, ref.Offset); err != nil {
|
|
return errors.Wrap(err, "write offset")
|
|
}
|
|
if err := binary.Write(w, binary.LittleEndian, ref.Time.UnixNano()); err != nil {
|
|
return errors.Wrap(err, "write time")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (idx *idx) DecodeFrom(ctx context.Context, r io.Reader) (err error) {
|
|
ctx = telemetry.StartWith(ctx, append(idx.TelemetryLabels(), telemetrymodel.KeyOperation, "DecodeFrom")...)
|
|
defer func() { telemetry.End(ctx, err) }()
|
|
idx.mu.Lock()
|
|
defer idx.mu.Unlock()
|
|
|
|
// Read header.
|
|
if err := idx.readHeader(r); err != nil {
|
|
return errors.Wrap(err, "read header")
|
|
}
|
|
|
|
// Read map count.
|
|
var count uint64
|
|
if err := binary.Read(r, binary.LittleEndian, &count); err != nil {
|
|
return errors.Wrap(err, "read map count")
|
|
}
|
|
idx.m = make(map[uint64]FeedRef, count)
|
|
|
|
// Read all key-value pairs.
|
|
for range count {
|
|
id, ref, err := idx.readEntry(r)
|
|
if err != nil {
|
|
return errors.Wrap(err, "read entry")
|
|
}
|
|
idx.m[id] = ref
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readHeader reads and validates the index file header.
|
|
func (idx *idx) readHeader(r io.Reader) error {
|
|
magicNumber := make([]byte, len(headerMagicNumber))
|
|
if _, err := io.ReadFull(r, magicNumber); err != nil {
|
|
return errors.Wrap(err, "read magic number")
|
|
}
|
|
if !bytes.Equal(magicNumber, headerMagicNumber) {
|
|
return errors.New("invalid magic number")
|
|
}
|
|
|
|
versionByte := make([]byte, 1)
|
|
if _, err := io.ReadFull(r, versionByte); err != nil {
|
|
return errors.Wrap(err, "read version")
|
|
}
|
|
if versionByte[0] != headerVersion {
|
|
return errors.New("invalid version")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readEntry reads a single key-value pair (feed ID and FeedRef) from the reader.
|
|
func (idx *idx) readEntry(r io.Reader) (id uint64, ref FeedRef, err error) {
|
|
// Read Key (ID).
|
|
if err := binary.Read(r, binary.LittleEndian, &id); err != nil {
|
|
return 0, FeedRef{}, errors.Wrap(err, "read id")
|
|
}
|
|
|
|
// Read Value (FeedRef).
|
|
if err := binary.Read(r, binary.LittleEndian, &ref.Chunk); err != nil {
|
|
return 0, FeedRef{}, errors.Wrap(err, "read chunk")
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &ref.Offset); err != nil {
|
|
return 0, FeedRef{}, errors.Wrap(err, "read offset")
|
|
}
|
|
var timestamp int64
|
|
if err := binary.Read(r, binary.LittleEndian, ×tamp); err != nil {
|
|
return 0, FeedRef{}, errors.Wrap(err, "read time")
|
|
}
|
|
ref.Time = time.Unix(0, timestamp).In(time.UTC)
|
|
|
|
return id, ref, nil
|
|
}
|
|
|
|
type mockIndex struct {
|
|
component.Mock
|
|
}
|
|
|
|
func (m *mockIndex) Search(ctx context.Context, id uint64) (ref FeedRef, ok bool) {
|
|
args := m.Called(ctx, id)
|
|
|
|
return args.Get(0).(FeedRef), args.Bool(1)
|
|
}
|
|
|
|
func (m *mockIndex) Add(ctx context.Context, id uint64, item FeedRef) {
|
|
m.Called(ctx, id, item)
|
|
}
|
|
|
|
func (m *mockIndex) IDs(ctx context.Context) (ids map[uint64]bool) {
|
|
args := m.Called(ctx)
|
|
|
|
return args.Get(0).(map[uint64]bool)
|
|
}
|
|
|
|
func (m *mockIndex) Count(ctx context.Context) (count uint32) {
|
|
args := m.Called(ctx)
|
|
|
|
return args.Get(0).(uint32)
|
|
}
|
|
|
|
func (m *mockIndex) EncodeTo(ctx context.Context, w io.Writer) (err error) {
|
|
args := m.Called(ctx, w)
|
|
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *mockIndex) DecodeFrom(ctx context.Context, r io.Reader) (err error) {
|
|
args := m.Called(ctx, r)
|
|
|
|
return args.Error(0)
|
|
}
|