mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-20 14:17:07 +00:00
* feat(s3/lifecycle): filer-backed cursor Persister FilerPersister persists per-shard cursor maps as JSON to /etc/s3/lifecycle/cursors/shard-NN.json via filer.SaveInsideFiler. One file per shard keeps Save atomic — the filer writes the entry in a single mutation, so a crash mid-write doesn't leak partial state. Pipeline.Run loads on start; the periodic checkpoint and graceful-shutdown save go through this implementation. A small FilerStore interface wraps the SeaweedFilerClient surface the persister needs, so tests inject an in-memory fake instead of mocking the full gRPC client. * refactor(s3/lifecycle): drop BlockerStore — durable cursor IS the block A frozen cursor doesn't advance, so the durable cursor (FilerPersister) encodes the blocked state on its own. On worker restart the reader re-encounters the poison event at MinTsNs, the dispatcher walks the same retry budget to BLOCKED, and the cursor freezes at the same EventTs. Other in-flight events between freeze tsNs and prior cursor positions self-resolve via NOOP_RESOLVED (STALE_IDENTITY) since the underlying objects were already deleted on the prior pass. Removed: - BlockerStore interface + InMemoryBlockerStore + BlockerRecord - Dispatcher.Blockers + Dispatcher.ReplayBlockers - the BlockerStore.Put call in handleBlocked - Pipeline.Blockers field + the ReplayBlockers call on startup Added a TestDispatchRestartReFreezesNaturally that pins the self-recovery property: a fresh Dispatcher with a fresh Cursor, fed the same poison event, reaches the same frozen state at the same EventTs without any durable blocker store. Operator visibility: a cursor whose MinTsNs hasn't advanced is the signal — surfaced via the durable cursor file. * refactor(filer): SaveInsideFiler accepts ctx ReadInsideFiler already takes ctx; SaveInsideFiler used context.Background() internally and silently dropped the caller's ctx. Symmetric API now; cancellation/deadlines propagate through LookupEntry / CreateEntry / UpdateEntry. Mechanical update of all callers — most pass context.Background() since the existing call sites have no ctx in scope. * fix(s3/lifecycle): deterministic order in cursor save Iterating Go maps yields random order, so json.Encode produced a different byte sequence on each save even when the state hadn't changed. Sort entries by (Bucket, ActionKind, RuleHash) before encoding so the on-disk file diffs cleanly. New test pins byte-identical output across two saves of the same map. * fix(s3/lifecycle): log reason when freezing cursor in handleBlocked handleBlocked dropped the reason via _ = reason with a comment claiming the caller logged it; none of the three callers do. A frozen cursor is the only surface where the operator finds out something stuck, so the reason has to land somewhere. glog.Warningf with shard, key, eventTs, and the original reason — same shape the rest of the package uses.
96 lines
3.6 KiB
Go
96 lines
3.6 KiB
Go
package offset
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer_client"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// FilerOffsetStorage implements OffsetStorage using SeaweedFS filer
|
|
// Stores offset data as files in the same directory structure as SMQ
|
|
// Path: /topics/{namespace}/{topic}/{version}/{partition}/checkpoint.offset
|
|
// The namespace and topic are derived from the actual partition information
|
|
type FilerOffsetStorage struct {
|
|
filerClientAccessor *filer_client.FilerClientAccessor
|
|
}
|
|
|
|
// NewFilerOffsetStorageWithAccessor creates a new filer-based offset storage using existing filer client accessor
|
|
func NewFilerOffsetStorageWithAccessor(filerClientAccessor *filer_client.FilerClientAccessor) *FilerOffsetStorage {
|
|
return &FilerOffsetStorage{
|
|
filerClientAccessor: filerClientAccessor,
|
|
}
|
|
}
|
|
|
|
// SaveCheckpoint saves the checkpoint for a partition
|
|
// Stores as: /topics/{namespace}/{topic}/{version}/{partition}/checkpoint.offset
|
|
func (f *FilerOffsetStorage) SaveCheckpoint(namespace, topicName string, partition *schema_pb.Partition, offset int64) error {
|
|
partitionDir := f.getPartitionDir(namespace, topicName, partition)
|
|
fileName := "checkpoint.offset"
|
|
|
|
// Use SMQ's 8-byte offset format
|
|
offsetBytes := make([]byte, 8)
|
|
util.Uint64toBytes(offsetBytes, uint64(offset))
|
|
|
|
return f.filerClientAccessor.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
return filer.SaveInsideFiler(context.Background(), client, partitionDir, fileName, offsetBytes)
|
|
})
|
|
}
|
|
|
|
// LoadCheckpoint loads the checkpoint for a partition
|
|
func (f *FilerOffsetStorage) LoadCheckpoint(namespace, topicName string, partition *schema_pb.Partition) (int64, error) {
|
|
partitionDir := f.getPartitionDir(namespace, topicName, partition)
|
|
fileName := "checkpoint.offset"
|
|
|
|
var offset int64 = -1
|
|
err := f.filerClientAccessor.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
data, err := filer.ReadInsideFiler(context.Background(), client, partitionDir, fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(data) != 8 {
|
|
return fmt.Errorf("invalid checkpoint file format: expected 8 bytes, got %d", len(data))
|
|
}
|
|
offset = int64(util.BytesToUint64(data))
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return offset, nil
|
|
}
|
|
|
|
// GetHighestOffset returns the highest offset stored for a partition
|
|
// For filer storage, this is the same as the checkpoint since we don't store individual records
|
|
func (f *FilerOffsetStorage) GetHighestOffset(namespace, topicName string, partition *schema_pb.Partition) (int64, error) {
|
|
return f.LoadCheckpoint(namespace, topicName, partition)
|
|
}
|
|
|
|
// Reset clears all data for testing
|
|
func (f *FilerOffsetStorage) Reset() error {
|
|
// For testing, we could delete all offset files, but this is dangerous
|
|
// Instead, just return success - individual tests should clean up their own data
|
|
return nil
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
// getPartitionDir returns the directory path for a partition following SMQ convention
|
|
// Format: /topics/{namespace}/{topic}/{version}/{partition}
|
|
func (f *FilerOffsetStorage) getPartitionDir(namespace, topicName string, partition *schema_pb.Partition) string {
|
|
// Generate version from UnixTimeNs
|
|
version := time.Unix(0, partition.UnixTimeNs).UTC().Format("v2006-01-02-15-04-05")
|
|
|
|
// Generate partition range string
|
|
partitionRange := fmt.Sprintf("%04d-%04d", partition.RangeStart, partition.RangeStop)
|
|
|
|
return fmt.Sprintf("%s/%s/%s/%s/%s", filer.TopicsDir, namespace, topicName, version, partitionRange)
|
|
}
|