Files
stream/storage/segment.go
T

204 lines
6.7 KiB
Go

package storage
import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
log "github.com/CefBoud/monkafka/logging"
"github.com/CefBoud/monkafka/serde"
"github.com/CefBoud/monkafka/state"
"github.com/CefBoud/monkafka/types"
)
const logSuffix = ".log"
const indexSuffix = ".index"
func getLogFile(topic string, partition uint32, baseOffset uint64) string {
return filepath.Join(GetPartitionDir(topic, partition), fmt.Sprintf("%020d", baseOffset)+".log")
}
func getIndexFile(topic string, partition uint32, baseOffset uint64) string {
return filepath.Join(GetPartitionDir(topic, partition), fmt.Sprintf("%020d", baseOffset)+".index")
}
// NewSegment create a new segment for the given partition
func NewSegment(p *types.Partition) (*types.Segment, error) {
var seg *types.Segment
segmentStartOffset := p.EndOffset()
if segmentStartOffset > 0 { // if partition's EndOffset == 0, no need to ++
segmentStartOffset++
}
indexFile, err := os.OpenFile(getIndexFile(p.TopicName, p.Index, segmentStartOffset), os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
log.Error("Error creating index file: %v", err)
return seg, err
}
logFile, err := os.OpenFile(getLogFile(p.TopicName, p.Index, segmentStartOffset), os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
log.Error("Error creating segment file: %v", err)
return seg, err
}
seg = &types.Segment{
LogFile: logFile,
IndexFile: indexFile,
StartOffset: segmentStartOffset,
EndOffset: segmentStartOffset,
}
return seg, nil
}
// LoadSegments loads segments with a base offset of [base_offset] would be stored in two files, a [base_offset].index and a [base_offset].log file.
func LoadSegments(partitionDir string) ([]*types.Segment, error) {
var segments []*types.Segment
entries, err := os.ReadDir(partitionDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), logSuffix) {
logFilePath := filepath.Join(partitionDir, entry.Name())
logFile, err := os.OpenFile(logFilePath, os.O_RDWR, 0644)
if err != nil {
return nil, fmt.Errorf("error opening logFile %v. %v", filepath.Join(partitionDir, entry.Name()), err)
}
indexFilePath := strings.Replace(logFilePath, logSuffix, indexSuffix, 1)
indexFile, err := os.OpenFile(indexFilePath, os.O_RDWR, 0644)
if err != nil {
return nil, fmt.Errorf("error opening logFile %v. %v", filepath.Join(partitionDir, entry.Name()), err)
}
indexData, err := io.ReadAll(indexFile)
if err != nil {
return nil, err
}
stat, err := logFile.Stat()
if err != nil {
return nil, fmt.Errorf("error reading log file info: %v", err)
}
logFileSize := uint32(stat.Size())
startOffset, err := strconv.ParseUint(strings.Replace(entry.Name(), logSuffix, "", 1), 10, 64)
if err != nil {
return nil, err
}
endOffset := startOffset
var lastRecordBatch types.RecordBatch
if len(indexData) > 0 {
lastRecordOffset := serde.Encoding.Uint32(indexData[len(indexData)-8:])
recordPosition := serde.Encoding.Uint32(indexData[len(indexData)-4:])
lastBatchRecordBytes := make([]byte, logFileSize-recordPosition)
_, err := logFile.ReadAt(lastBatchRecordBytes, int64(recordPosition))
if err != nil {
return nil, fmt.Errorf("error while reading %v. %v ", logFilePath, err)
}
lastRecordBatch = ReadRecordBatch(lastBatchRecordBytes)
// offset written in the index are relative to the start, hence endOffset += not endOffset =
endOffset += uint64(lastRecordOffset + lastRecordBatch.LastOffsetDelta)
}
log.Info("loading segment %v EndOffset %v indexData len %v\n ", indexFilePath, endOffset, len(indexData))
segments = append(segments, &types.Segment{
LogFile: logFile,
IndexFile: indexFile,
IndexData: indexData,
StartOffset: startOffset,
EndOffset: endOffset,
LogFileSize: logFileSize,
MaxTimestamp: lastRecordBatch.MaxTimestamp,
})
}
}
return segments, nil
}
func shouldRollSegment(segment *types.Segment) bool {
// roll if larger than LogSegmentSizeBytes or older than LogSegmentMs
return segment.LogFileSize >= uint32(state.Config.LogSegmentSizeBytes) ||
(segment.MaxTimestamp > 0 && segment.MaxTimestamp < uint64(time.Now().UnixMilli())-state.Config.LogSegmentMs)
}
func shouldDeleteOldestSegment(partition *types.Partition) bool {
segment := partition.Segments[0]
return segment.MaxTimestamp > 0 && segment.MaxTimestamp < uint64(time.Now().UnixMilli())-state.Config.LogRetentionMs
}
func rollPartitionSegment(partition *types.Partition) error {
activeSegment := partition.ActiveSegment()
activeSegment.Lock()
defer activeSegment.Unlock()
seg, err := NewSegment(partition)
if err != nil {
return fmt.Errorf("error while creating new segment for roll %v", err)
}
partition.Segments = append(partition.Segments, seg)
return nil
}
func deleteOldestSegment(partition *types.Partition) error {
segment := partition.Segments[0]
segment.Lock()
defer segment.Unlock()
err := os.Remove(segment.IndexFile.Name())
if err != nil {
return fmt.Errorf("failed to move %v with error: %v", segment.IndexFile.Name(), err)
}
err = os.Remove(segment.LogFile.Name())
if err != nil {
return fmt.Errorf("failed to move %v with error: %v", segment.LogFile.Name(), err)
}
partition.Segments = partition.Segments[1:]
return nil
}
func cleanupPartition(partition *types.Partition) error {
partition.Lock()
defer partition.Unlock()
activeSegment := partition.ActiveSegment()
if activeSegment != nil {
if shouldRollSegment(activeSegment) {
log.Info("rolling segment for partition %v-%v \n", partition.TopicName, partition.Index)
err := rollPartitionSegment(partition)
if err != nil {
return fmt.Errorf("error while rolling partition %v-%v segment %v", partition.TopicName, partition.Index, err)
}
}
} else {
log.Error("cleanupPartition: partition %v-%v has no active segment", partition.TopicName, partition.Index)
}
if len(partition.Segments) > 1 {
if shouldDeleteOldestSegment(partition) {
log.Info("deleting oldest segment for partition %v-%v \n", partition.TopicName, partition.Index)
err := deleteOldestSegment(partition)
if err != nil {
return fmt.Errorf("error while deleting oldest segment for partition %v-%v Error: %v", partition.TopicName, partition.Index, err)
}
}
}
return nil
}
// CleanupSegments takes care of segment retention based on time and size
func CleanupSegments() error {
log.Info("Running CleanupSegments")
for _, partitionMap := range state.TopicStateInstance {
for _, partition := range partitionMap {
if err := cleanupPartition(partition); err != nil {
return fmt.Errorf("error while cleanupPartition %v-%v Error: %v", partition.TopicName, partition.Index, err)
}
}
}
return nil
}