mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 15:00:48 +01:00
* filer: enable per-path commit records only when a peer has its own store Since seaweedfs#11698 every metadata change writes a per-path commit record into the filer store (an MpTs key, plus an MpDl key on deletes), and the records stay after the path is deleted. They order the events a filer replays from peer filers that keep their own store, since replay only runs when peerSignature != f.Signature. Filers that all share one store (one Redis, MySQL, TiKV, ...) never replay each other's events, so the records are never read, yet each change still costs one or two extra store writes and the key count grows with every path ever changed — unbounded growth for in-memory stores. This turns the records off by default and latches them on the first time a peer reports a different store signature — no option needed. The latch is persisted in the store (MpRep) so it survives restarts and converges filers sharing a store even before they discover the peer themselves. While off, a filer keeps an MpOff marker so enabling later bumps the records' generation (MpGen, folded into each record key) past the unrecorded window: a record older than a change made while off cannot let an older replayed peer event win. Ordering for unrecorded changes falls back to the entry's mtime, the same as entries written before the records existed. Supersedes the manual option approach in seaweedfs#12060. * filer: keep delete tombstones while records off, harden enable path Review on the first commit: - A delete committed while the records were off left no entry and no usable tombstone: after enabling, an older replayed create could resurrect the path. Deletes and rename sources are now always recorded under the generation-0 MpDl key — they are never fenced by a generation bump, and they cannot go stale because every delete is recorded. Creates/updates still write nothing while off. - bumpPathCommitGen could regress MpGen under concurrent bumpers on a shared store. It now loops: each pass writes only a generation higher than any it has seen, so concurrent bumps converge upward. - doSubscribeToOneFiler proceeded into replay when enablePathCommitRecords failed, running the whole session without ordering. It now aborts the subscribe so the reconnect retries with the records on. * filer: bind pending commit-record repairs to their generation A failed MpTs write kept in metaTsPending could land after a shared-store generation bump, writing a pre-bump timestamp as a current-generation record and overriding newer unrecorded state. Each pending change record now carries the generation it failed under: repairs write it only while that generation is current, and ordering reads ignore it otherwise. Pending delete records need no binding — MpDl keys are never fenced. * filer: never move the local records' generation backward The converge loop in bumpPathCommitGen seeded from zero and both it and syncPathCommitGen stored unconditionally: a stale writer could leave the stored generation below what this filer already adopted, and the next sync or bump then walked the local value back — orphaning records it wrote under the higher generation. The bump now seeds from the local generation and every store only moves it forward. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: name a records generation after the off marker it consumes Two filers consuming the same off marker each counted one past the stored generation, so the second could store N+2 after the first stored N+1 and write records under a key the other could not read until the next check. The bump now takes the marker's own timestamp — filers racing on one marker land on one value — a marker older than the stored generation is cleaned as a leftover, and a stored value that already passed the bump is adopted instead of counted past. A newly enabling filer whose marker was already consumed names its own off window with the current time, so its unrecorded period is still fenced. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: tell a fresh off marker from one the generation already consumed A marker whose timestamp reads at or below the stored generation was dropped as leftover, but a new off period can also read that old — the off filer's clock lagging, or a host clock moving back — and dropping it kept records from before that period live, so stale replay could win. The stored generation now names the marker it was bumped for: a marker the store still shows is consumed unless it is that one, however old it reads, and only the consumed marker itself is cleared without a bump. The generation trails in the value, so a whole-slice read still yields it on stores written before the pair was stored. Enabling fences the filer's own off window with the current time instead of a marker a sharer may already have consumed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: cover racing off-marker consumers settling on one generation * filer: adopt the stored generation, not the consumed marker it names A filer syncing with no off marker present read the value's first field — the consumed marker's timestamp — so a restart after a lagging-clock off period under-counted the generation and kept records the bump dropped. * filer: re-check the off marker before consuming it A sync whose marker read ran behind a later off period's consume cycle still bumped for its stale read: the stored generation already covered a different marker, so the bump overwrote the consumed name, counted the generation past records a newer consumption meant to keep, and cleared whatever marker was live by then. Each pass now re-reads the marker and only bumps or clears while it is still the one the pass saw. * filer: keep a failed generation read from resetting the records' generation The final generation read ignored errors and then stored whatever gen held — possibly zero — so a transient store failure could hide records the current generation wrote. Unexpected read errors now return before the local generation changes. Also cover a sync whose marker read runs behind another consumer's delete: it must adopt the stored generation, not bump for the stale read. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: fail a bump that cannot verify the stored generation A generation read that failed mid-bump was treated as absent: the write could land below a generation a racing bumper had stored, or re-store a marker that bumper had already consumed. Read failures now abort the pass like every other store error on the path. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: cover generation reads that fail instead of returning a value Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: carry the commit timestamp on the entry itself The per-path MpTs record only exists because a commit timestamp had nowhere else to live. Stamping commit_ts_ns on the entry before its store write - and reusing that stamp as the emitted event's TsNs - makes the entry its own commit record: replay and bootstrap traversal order on it with zero extra writes for creates and updates. Deletes still need a durable key since a removed entry cannot carry a timestamp, so MpDl tombstones stay (one write per delete, growth bounded by delete history). Entries predating the field fall back to mtime, matching what peers and old records already assume. This replaces the MpTs/MpOff/MpGen/MpRep machinery entirely: shared- store filers write nothing extra, and dedicated-store replication keeps last-writer-wins ordering plus tombstone coverage for deletes and ancestor drops. * filer: keep a directory update replay from tombstoning its subtree A same-path update wrote a delete record before the delete-and-reinsert, so a directory metadata change read as a subtree delete: a later peer event for a child older than the update was then skipped even though nobody deleted the children, leaving files missing on this filer. Only file updates take the tombstone now — the re-inserted directory entry's own commit stamp orders the path itself. * filer: order unstamped entries by their recorded commit before mtime Entries written before stamping existed have no CommitTsNs, so the ordering check fell back to the client-settable mtime and dropped the MpTs record those versions wrote: an entry committed at event time 300 with mtime 100 lost to a delayed peer event at 200 after upgrade. Read the legacy record for unstamped entries in localCommitTsNs, CommitTsNs, and the traverse stamp (which also stopped falling back to mtime, regressing entry_ts_ns to 0). * filer: release queued rename event stamps when the rename fails Each moved entry carried an in-flight stamp its queued event would adopt at emit time. When the move succeeded but a later child move or the commit failed, the handler rolled back without emitting the queued events or releasing their stamps, pinning the delivered and flushed watermarks below the aborted rename forever. Release every queued stamp on both failure exits in both rename handlers. * filer: apply a directory update replay with an in-place upsert Deleting the stored entry before inserting its replacement left a window where the directory had no entry and no ordering record — a tombstone cannot stand in for it without reading as a subtree delete that suppresses the children's own records. A stale peer event accepted in that gap could replay an older update or delete and sweep the directory's children. Upsert the replacement instead: the entry keeps its recorded commit order across the whole update, and a directory has no chunks or hard links for a delete to release. * filer: update an existing directory entry on replay instead of upserting InsertEntry is not an upsert on SQL stores configured without upsert support — PostgreSQL with enableUpsert=false issues a plain INSERT and its duplicate-key error does not match the MySQL-only fallback, so a replay updating a present directory failed on every retry. Pick UpdateEntry when the entry exists and InsertEntry when it is absent; UpdateEntry touches no row that is not there. * filer: detect a shared-store delete racing a replayed directory update UpdateEntry reports success even when the row was deleted between the find and the update — the UPDATE changes zero rows and the replayed change is dropped while the subscription offset advances. Re-read the entry after the update and retry the write, re-checking the recorded commit order each pass so a newer peer change wins instead of being resurrected over. * filer: declare commit_ts_ns in field-number order Field 6 sits between extended and hard_link_id; the declaration now reads in the same order. Wire format unchanged. Also syncs the Java client's checked-in copy of filer.proto, which the pb Makefile copies on gen. * filer: option to skip delete tombstones entirely filer.options.deleteTombstones=false stops writing MpDl records on deletes - a single filer or shared-store cluster never replays peer events, so the tombstones are dead weight. The tradeoff is explicit: if a dedicated-store peer does replicate, a stale event can resurrect a deleted path. * filer: drop delete tombstones entirely Deletes no longer write MpDl records, and the pending-repair machinery they needed is gone with them: per-mutation metadata overhead is now zero for every store topology. Ordering information lives only on the entry itself via commit_ts_ns. The accepted tradeoff: a dedicated-store peer replaying an event older than a delete can resurrect the path, and a kept directory no longer fences stale child creates. Recorded tombstones and MpTs records left by older builds are still read, so upgrades keep the protections they already have. * filer: stop reading legacy per-path records too Drops the MpTs/MpDl read path along with the writes: meta_path_ts.go keeps only the entry-field readers (CommitTsNs, EntryCommitTsNs, localCommitTsNs). Entries written before stamping existed fall back to mtime, and tombstones or commit records older builds left behind no longer fence replayed events. * filer: trim commit timestamp comments --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
894 lines
31 KiB
Go
894 lines
31 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/log_buffer"
|
|
)
|
|
|
|
type MetaAggregator struct {
|
|
filer *Filer
|
|
self pb.ServerAddress
|
|
isLeader bool
|
|
grpcDialOption grpc.DialOption
|
|
MetaLogBuffer *log_buffer.LogBuffer
|
|
peerChans map[pb.ServerAddress]chan struct{}
|
|
peerChansLock sync.Mutex
|
|
// peerWatermarks tracks, per subscribed peer (self included), the newest
|
|
// timestamp received on that peer's stream (event or idle heartbeat).
|
|
// The minimum is a delivery low-watermark: MetaLogBuffer is complete up
|
|
// to it, so a subscriber held at or below it cannot miss a late-merged
|
|
// peer event. An unsignalled peer pins it at zero.
|
|
peerWatermarks map[pb.ServerAddress]int64
|
|
// peerFlushWatermarks tracks each peer's reported flush watermark:
|
|
// everything at or below it is on that peer's disk. The minimum bounds
|
|
// persisted-log reads — beyond it a peer may still land a log file (or
|
|
// chunk) whose events a passed cursor would skip.
|
|
peerFlushWatermarks map[pb.ServerAddress]int64
|
|
// peerRemovedAtNs marks peers the master removed, with the removal time.
|
|
// A removed peer keeps participating in the low-watermarks for a grace
|
|
// period: removal usually means a flap (frozen or partitioned filer)
|
|
// whose unflushed events still exist, and de-accounting it at once would
|
|
// let subscribers advance past them. A re-add clears the mark; a peer
|
|
// gone past the grace is dropped so it cannot pin the low-watermarks.
|
|
peerRemovedAtNs map[pb.ServerAddress]int64
|
|
// lowWatermarkTsNs and lowFlushWatermarkTsNs are the last minima signalled
|
|
// on deliveryAdvanced and flushAdvanced, each closed and replaced when its
|
|
// own minimum rises. Held readers park on the one that bounds them:
|
|
// arriving data cannot release a watermark hold, only a peer reporting
|
|
// the progress that hold waits on. The two are kept apart because a
|
|
// delivery advance - one per event a peer streams - cannot release a read
|
|
// held at the flush watermark, and waking it costs a whole pass.
|
|
lowWatermarkTsNs int64
|
|
lowFlushWatermarkTsNs int64
|
|
deliveryAdvanced chan struct{}
|
|
flushAdvanced chan struct{}
|
|
remotePeerArrived chan struct{}
|
|
peerWatermarksLock sync.Mutex
|
|
peerWg sync.WaitGroup
|
|
// stopped is set under peerChansLock by Shutdown; OnPeerUpdate then
|
|
// rejects adds so no subscription can outlive the shutdown wait and run
|
|
// against a store Filer.Shutdown has already closed.
|
|
stopped bool
|
|
}
|
|
|
|
// MetaAggregator only aggregates data "on the fly". The logs are not re-persisted to disk.
|
|
// The old data comes from what each LocalMetadata persisted on disk.
|
|
func NewMetaAggregator(filer *Filer, self pb.ServerAddress, grpcDialOption grpc.DialOption) *MetaAggregator {
|
|
t := &MetaAggregator{
|
|
filer: filer,
|
|
self: self,
|
|
grpcDialOption: grpcDialOption,
|
|
peerChans: make(map[pb.ServerAddress]chan struct{}),
|
|
peerWatermarks: make(map[pb.ServerAddress]int64),
|
|
peerFlushWatermarks: make(map[pb.ServerAddress]int64),
|
|
peerRemovedAtNs: make(map[pb.ServerAddress]int64),
|
|
}
|
|
// nil notifyFn: aggregated subscribers wake through the buffer's
|
|
// subscriber channels, not a cond.
|
|
t.MetaLogBuffer = log_buffer.NewLogBuffer("aggr", LogFlushInterval, nil, nil, nil)
|
|
return t
|
|
}
|
|
|
|
func (ma *MetaAggregator) OnPeerUpdate(update *master_pb.ClusterNodeUpdate, startFrom time.Time) {
|
|
ma.peerChansLock.Lock()
|
|
defer ma.peerChansLock.Unlock()
|
|
|
|
// After Shutdown the master connection may still deliver an update; a
|
|
// subscription started now would have no stop signal and could run past
|
|
// the store close.
|
|
if ma.stopped {
|
|
return
|
|
}
|
|
|
|
address := pb.ServerAddress(update.Address)
|
|
if update.IsAdd {
|
|
// the peer is already followed, restarting would only lose the events
|
|
// in between
|
|
if _, found := ma.peerChans[address]; found {
|
|
return
|
|
}
|
|
stopChan := make(chan struct{})
|
|
ma.peerChans[address] = stopChan
|
|
if address != ma.self {
|
|
ma.noteRemotePeerArrivalLocked()
|
|
}
|
|
// Account for the peer before its stream signals; keep prior values
|
|
// on reconnect.
|
|
ma.initPeerWatermark(address)
|
|
ma.peerWg.Add(1)
|
|
go func() {
|
|
defer ma.peerWg.Done()
|
|
ma.loopSubscribeToOneFiler(ma.filer, ma.self, address, startFrom, stopChan)
|
|
}()
|
|
} else {
|
|
if prevChan, found := ma.peerChans[address]; found {
|
|
close(prevChan)
|
|
delete(ma.peerChans, address)
|
|
}
|
|
// Only mark: dropping the watermarks at once would let subscribers
|
|
// advance past a flapping peer's unflushed events (see peerRemovedAtNs).
|
|
ma.markPeerWatermarkRemoved(address)
|
|
}
|
|
}
|
|
|
|
// peerWatermarkRemovalGrace is how long a removed peer keeps participating
|
|
// in the low-watermarks. Matches the subscribe loops' settled horizon, which
|
|
// already bounds a stale watermark's influence within it.
|
|
const peerWatermarkRemovalGrace = 2 * LogFlushInterval
|
|
|
|
// initPeerWatermark adds the peer with a zero (unknown) watermark; a re-added
|
|
// peer keeps its prior values and stops being considered removed.
|
|
func (ma *MetaAggregator) initPeerWatermark(peer pb.ServerAddress) {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
if _, found := ma.peerWatermarks[peer]; !found {
|
|
ma.peerWatermarks[peer] = 0
|
|
}
|
|
if _, found := ma.peerFlushWatermarks[peer]; !found {
|
|
ma.peerFlushWatermarks[peer] = 0
|
|
}
|
|
delete(ma.peerRemovedAtNs, peer)
|
|
ma.noteLowWatermarksLocked()
|
|
}
|
|
|
|
func (ma *MetaAggregator) markPeerWatermarkRemoved(peer pb.ServerAddress) {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
if _, found := ma.peerWatermarks[peer]; !found {
|
|
return
|
|
}
|
|
// First removal time wins: duplicate removals must not refresh the grace.
|
|
if _, marked := ma.peerRemovedAtNs[peer]; !marked {
|
|
ma.peerRemovedAtNs[peer] = time.Now().UnixNano()
|
|
}
|
|
}
|
|
|
|
// dropExpiredRemovedPeersLocked drops peers whose removal outlived the grace.
|
|
// Caller must hold peerWatermarksLock.
|
|
func (ma *MetaAggregator) dropExpiredRemovedPeersLocked() {
|
|
if len(ma.peerRemovedAtNs) == 0 {
|
|
return
|
|
}
|
|
cutoff := time.Now().UnixNano() - int64(peerWatermarkRemovalGrace)
|
|
for peer, removedAt := range ma.peerRemovedAtNs {
|
|
if removedAt < cutoff {
|
|
delete(ma.peerRemovedAtNs, peer)
|
|
delete(ma.peerWatermarks, peer)
|
|
delete(ma.peerFlushWatermarks, peer)
|
|
}
|
|
}
|
|
// Dropping the peer that pinned a minimum raises it, same as it reporting.
|
|
ma.noteLowWatermarksLocked()
|
|
}
|
|
|
|
// advancePeerWatermark records the peer as received-through tsNs. Monotonic,
|
|
// and only for tracked peers: a dropped peer's straggler must not recreate
|
|
// its entry and pin the low-watermark.
|
|
func (ma *MetaAggregator) advancePeerWatermark(peer pb.ServerAddress, tsNs int64) {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
if cur, found := ma.peerWatermarks[peer]; found && tsNs > cur {
|
|
ma.peerWatermarks[peer] = tsNs
|
|
ma.noteLowWatermarksLocked()
|
|
}
|
|
}
|
|
|
|
// advancePeerFlushWatermark records the peer's reported flush watermark.
|
|
// Monotonic, and only for tracked peers (see advancePeerWatermark).
|
|
func (ma *MetaAggregator) advancePeerFlushWatermark(peer pb.ServerAddress, tsNs int64) {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
if cur, found := ma.peerFlushWatermarks[peer]; found && tsNs > cur {
|
|
ma.peerFlushWatermarks[peer] = tsNs
|
|
ma.noteLowWatermarksLocked()
|
|
}
|
|
}
|
|
|
|
// PeerLowFlushWatermarkTsNs returns the minimum reported flush watermark
|
|
// across all current peers: every peer's events at or below this time are on
|
|
// disk. Returns 0 when any peer has not reported yet (completeness unknown).
|
|
func (ma *MetaAggregator) PeerLowFlushWatermarkTsNs() int64 {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
ma.dropExpiredRemovedPeersLocked()
|
|
return lowWatermarkOf(ma.peerFlushWatermarks)
|
|
}
|
|
|
|
// PeerLowWatermarkTsNs returns the minimum received-through timestamp across
|
|
// all current peers (self included): MetaLogBuffer is complete up to this
|
|
// time. Returns 0 when any peer has not signalled yet (or none are tracked),
|
|
// i.e. completeness is unknown.
|
|
func (ma *MetaAggregator) PeerLowWatermarkTsNs() int64 {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
ma.dropExpiredRemovedPeersLocked()
|
|
return lowWatermarkOf(ma.peerWatermarks)
|
|
}
|
|
|
|
// DeliveryWatermarkAdvancedChan returns a channel closed the next time the
|
|
// delivery low-watermark rises, which is what releases an in-memory read held
|
|
// at it. PeerLowFlushWatermarkTsNs has FlushWatermarkAdvancedChan. Callers
|
|
// must take the channel before reading the watermark they hold at, so a rise
|
|
// in between wakes them instead of being missed.
|
|
func (ma *MetaAggregator) DeliveryWatermarkAdvancedChan() <-chan struct{} {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
if ma.deliveryAdvanced == nil {
|
|
ma.deliveryAdvanced = make(chan struct{})
|
|
}
|
|
return ma.deliveryAdvanced
|
|
}
|
|
|
|
// FlushWatermarkAdvancedChan returns a channel closed the next time the flush
|
|
// low-watermark rises, which is what releases a persisted-log read held at it.
|
|
func (ma *MetaAggregator) FlushWatermarkAdvancedChan() <-chan struct{} {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
if ma.flushAdvanced == nil {
|
|
ma.flushAdvanced = make(chan struct{})
|
|
}
|
|
return ma.flushAdvanced
|
|
}
|
|
|
|
// noteLowWatermarksLocked recomputes both minima and wakes the readers parked
|
|
// on each one that rose. Caller must hold peerWatermarksLock.
|
|
func (ma *MetaAggregator) noteLowWatermarksLocked() {
|
|
low, flushLow := lowWatermarkOf(ma.peerWatermarks), lowWatermarkOf(ma.peerFlushWatermarks)
|
|
// Falls are recorded too (a joining peer sits at 0 until it signals), so
|
|
// the climb back out of one is seen as a rise.
|
|
if low > ma.lowWatermarkTsNs {
|
|
ma.deliveryAdvanced = closeWatermarkChan(ma.deliveryAdvanced)
|
|
}
|
|
if flushLow > ma.lowFlushWatermarkTsNs {
|
|
ma.flushAdvanced = closeWatermarkChan(ma.flushAdvanced)
|
|
}
|
|
ma.lowWatermarkTsNs, ma.lowFlushWatermarkTsNs = low, flushLow
|
|
}
|
|
|
|
// closeWatermarkChan wakes everyone parked on ch and clears it, so the next
|
|
// caller parks on a fresh one.
|
|
func closeWatermarkChan(ch chan struct{}) chan struct{} {
|
|
if ch != nil {
|
|
close(ch)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// lowWatermarkOf returns the minimum across the tracked peers, or 0 when none
|
|
// are tracked: a peer that has not signalled sits at 0 and pins the minimum
|
|
// there, which is what makes completeness unknown.
|
|
func lowWatermarkOf(watermarks map[pb.ServerAddress]int64) int64 {
|
|
if len(watermarks) == 0 {
|
|
return 0
|
|
}
|
|
var low int64 = math.MaxInt64
|
|
for _, tsNs := range watermarks {
|
|
if tsNs < low {
|
|
low = tsNs
|
|
}
|
|
}
|
|
return low
|
|
}
|
|
|
|
// PeersCaughtUpTo reports whether every listed peer's stream has delivered
|
|
// an event or idle heartbeat at or past tsNs, i.e. no pre-tsNs backlog can
|
|
// still arrive from it. A peer no longer tracked counts as caught up: it
|
|
// cannot deliver a backlog anymore.
|
|
func (ma *MetaAggregator) PeersCaughtUpTo(peers []pb.ServerAddress, tsNs int64) bool {
|
|
ma.peerWatermarksLock.Lock()
|
|
defer ma.peerWatermarksLock.Unlock()
|
|
for _, peer := range peers {
|
|
if ts, found := ma.peerWatermarks[peer]; found && ts < tsNs {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (ma *MetaAggregator) HasRemotePeers() bool {
|
|
ma.peerChansLock.Lock()
|
|
defer ma.peerChansLock.Unlock()
|
|
return ma.hasRemotePeersLocked()
|
|
}
|
|
|
|
func (ma *MetaAggregator) hasRemotePeersLocked() bool {
|
|
for address := range ma.peerChans {
|
|
if address != ma.self {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RemotePeerArrivedChan returns a channel closed when the aggregator learns
|
|
// its first remote peer, or nil if one is already known.
|
|
func (ma *MetaAggregator) RemotePeerArrivedChan() <-chan struct{} {
|
|
ma.peerChansLock.Lock()
|
|
defer ma.peerChansLock.Unlock()
|
|
if ma.hasRemotePeersLocked() {
|
|
return nil
|
|
}
|
|
return ma.remotePeerArrivedChanLocked()
|
|
}
|
|
|
|
// remotePeerArrivedChanLocked arms and returns the arrival channel. Caller must hold peerChansLock.
|
|
func (ma *MetaAggregator) remotePeerArrivedChanLocked() <-chan struct{} {
|
|
if ma.remotePeerArrived == nil {
|
|
ma.remotePeerArrived = make(chan struct{})
|
|
}
|
|
return ma.remotePeerArrived
|
|
}
|
|
|
|
// noteRemotePeerArrivalLocked closes the arrival channel when a remote peer is added. Caller must hold peerChansLock.
|
|
func (ma *MetaAggregator) noteRemotePeerArrivalLocked() {
|
|
ma.remotePeerArrived = closeWatermarkChan(ma.remotePeerArrived)
|
|
}
|
|
|
|
// HasPeer reports whether address is currently a tracked filer peer (or this
|
|
// filer's own address). Callers use this to gate operations on known cluster
|
|
// members.
|
|
func (ma *MetaAggregator) HasPeer(address pb.ServerAddress) bool {
|
|
if address == ma.self || address.Equals(ma.self) {
|
|
return true
|
|
}
|
|
ma.peerChansLock.Lock()
|
|
defer ma.peerChansLock.Unlock()
|
|
for peer := range ma.peerChans {
|
|
if peer == address || peer.Equals(address) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Shutdown stops every peer subscription and waits for each to return,
|
|
// letting the deferred offset save persist the last applied position before
|
|
// the store closes. Without it a graceful restart resumes from the last
|
|
// periodically-saved offset — up to a minute stale — and replays those
|
|
// events over newer local state.
|
|
func (ma *MetaAggregator) Shutdown() {
|
|
ma.peerChansLock.Lock()
|
|
ma.stopped = true
|
|
chans := ma.peerChans
|
|
ma.peerChans = make(map[pb.ServerAddress]chan struct{})
|
|
for _, stopChan := range chans {
|
|
close(stopChan)
|
|
}
|
|
ma.peerChansLock.Unlock()
|
|
|
|
// Wait without a cap: every blocking point in the subscribe loop is
|
|
// stop-aware, so a peer still running here is genuinely wedged and
|
|
// returning anyway would let Filer.Shutdown close the store under it and
|
|
// skip its deferred offset save.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
ma.peerWg.Wait()
|
|
close(done)
|
|
}()
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-ticker.C:
|
|
glog.Warningf("meta aggregator shutdown: peer subscriptions still stopping")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ma *MetaAggregator) loopSubscribeToOneFiler(f *Filer, self pb.ServerAddress, peer pb.ServerAddress, startFrom time.Time, stopChan chan struct{}) {
|
|
lastTsNs := startFrom.UnixNano()
|
|
for {
|
|
glog.V(0).Infof("loopSubscribeToOneFiler read %s start from %v %d", peer, time.Unix(0, lastTsNs), lastTsNs)
|
|
nextLastTsNs, err := ma.doSubscribeToOneFiler(f, self, peer, lastTsNs, stopChan)
|
|
|
|
// check stopChan to see if we should stop
|
|
select {
|
|
case <-stopChan:
|
|
glog.V(0).Infof("stop subscribing peer %s meta change", peer)
|
|
return
|
|
default:
|
|
}
|
|
|
|
if err != nil {
|
|
errLvl := glog.Level(0)
|
|
if strings.Contains(err.Error(), "duplicated local subscription detected") {
|
|
errLvl = glog.Level(4)
|
|
}
|
|
glog.V(errLvl).Infof("subscribing remote %s meta change: %v", peer, err)
|
|
}
|
|
if lastTsNs < nextLastTsNs {
|
|
lastTsNs = nextLastTsNs
|
|
}
|
|
select {
|
|
case <-stopChan:
|
|
glog.V(0).Infof("stop subscribing peer %s meta change", peer)
|
|
return
|
|
case <-time.After(1733 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ma *MetaAggregator) doSubscribeToOneFiler(f *Filer, self pb.ServerAddress, peer pb.ServerAddress, startFrom int64, stopChan <-chan struct{}) (int64, error) {
|
|
|
|
/*
|
|
Each filer reads the "filer.store.id", which is the store's signature when filer starts.
|
|
|
|
When reading from other filers' local meta changes:
|
|
* if the received change does not contain signature from self, apply the change to current filer store.
|
|
|
|
Upon connecting to other filers, need to remember their signature and their offsets.
|
|
|
|
*/
|
|
|
|
var maybeReplicateMetadataChange func(*filer_pb.SubscribeMetadataResponse) error
|
|
lastPersistTime := time.Now()
|
|
lastTsNs := startFrom
|
|
// replayFloorTsNs is the earliest event this session gave up replaying.
|
|
// The persisted offset and the reconnect resume point must stay behind
|
|
// it so the event is redelivered on the next subscribe instead of being
|
|
// skipped forever.
|
|
var replayFloorTsNs int64
|
|
persistableTsNs := func(tsNs int64) int64 {
|
|
if replayFloorTsNs > 0 && replayFloorTsNs-1 < tsNs {
|
|
return replayFloorTsNs - 1
|
|
}
|
|
return tsNs
|
|
}
|
|
|
|
// Everything below derives from ctx, cancelled when the peer is removed
|
|
// or the aggregator shuts down, so no stage can block past its stop.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go func() {
|
|
select {
|
|
case <-stopChan:
|
|
cancel()
|
|
case <-ctx.Done():
|
|
}
|
|
}()
|
|
|
|
peerSignature, err := ma.readFilerStoreSignature(ctx, peer)
|
|
if err != nil {
|
|
return lastTsNs, fmt.Errorf("connecting to peer filer %s: %v", peer, err)
|
|
}
|
|
|
|
// when filer store is not shared by multiple filers
|
|
if peerSignature != f.Signature {
|
|
if prevTsNs, err := ma.readOffset(f, peer, peerSignature); err == nil {
|
|
lastTsNs = prevTsNs
|
|
} else if errors.Is(err, ErrKvNotFound) {
|
|
// No stored offset — this is the first time connecting to this peer.
|
|
// Traverse the peer's full metadata tree so we get pre-existing data.
|
|
// Record time before traversal and subtract a safety margin to
|
|
// account for clock skew between this filer and the peer. Any
|
|
// duplicate events replayed during the overlap are harmless since
|
|
// Replay does upserts. We use wall-clock time (same domain as the
|
|
// metadata stream TsNs) rather than entry Mtime which is a
|
|
// different concept and can be set to arbitrary values.
|
|
preTraverseTime := time.Now()
|
|
glog.V(0).Infof("no previous offset for peer %s, starting full metadata sync", peer)
|
|
if traverseErr := ma.traversePeerMetadata(ctx, f, peer); traverseErr != nil {
|
|
return lastTsNs, fmt.Errorf("initial metadata sync from %s: %v", peer, traverseErr)
|
|
}
|
|
lastTsNs = preTraverseTime.Add(-time.Minute).UnixNano()
|
|
if err := ma.updateOffset(f, peer, peerSignature, lastTsNs); err != nil {
|
|
return lastTsNs, fmt.Errorf("save bootstrap offset for peer %s: %w", peer, err)
|
|
}
|
|
glog.V(0).Infof("completed full metadata sync from peer %s, will stream changes from %v", peer, time.Unix(0, lastTsNs))
|
|
} else {
|
|
return lastTsNs, fmt.Errorf("read offset for peer %s: %w", peer, err)
|
|
}
|
|
defer func(prevTsNs int64) {
|
|
saveTsNs := persistableTsNs(lastTsNs)
|
|
if saveTsNs != prevTsNs && lastTsNs != lastPersistTime.UnixNano() {
|
|
if err := ma.updateOffset(f, peer, peerSignature, saveTsNs); err == nil {
|
|
glog.V(0).Infof("last sync time with %s at %v (%d)", peer, time.Unix(0, saveTsNs), saveTsNs)
|
|
} else {
|
|
glog.Errorf("failed to save last sync time with %s at %v (%d)", peer, time.Unix(0, saveTsNs), saveTsNs)
|
|
}
|
|
}
|
|
}(lastTsNs)
|
|
|
|
glog.V(0).Infof("follow peer: %v, last %v (%d)", peer, time.Unix(0, lastTsNs), lastTsNs)
|
|
var counter int64
|
|
var synced bool
|
|
maybeReplicateMetadataChange = func(event *filer_pb.SubscribeMetadataResponse) error {
|
|
if err := replicateMetadataChange(f, peer, event, stopChan); err != nil {
|
|
if errors.Is(err, errPeerSubscriptionStopped) {
|
|
return err
|
|
}
|
|
// A failed event becomes a barrier the persisted offset and
|
|
// the reconnect resume point cannot pass, while the stream
|
|
// keeps flowing: blocking on an event that can never replay
|
|
// would stall every later event from this peer.
|
|
if replayFloorTsNs == 0 || event.TsNs < replayFloorTsNs {
|
|
replayFloorTsNs = event.TsNs
|
|
}
|
|
return nil
|
|
}
|
|
counter++
|
|
if lastPersistTime.Add(time.Minute).Before(time.Now()) {
|
|
if err := ma.updateOffset(f, peer, peerSignature, persistableTsNs(event.TsNs)); err == nil {
|
|
if event.TsNs < time.Now().Add(-2*time.Minute).UnixNano() {
|
|
glog.V(0).Infof("sync with %s progressed to: %v %0.2f/sec", peer, time.Unix(0, event.TsNs), float64(counter)/60.0)
|
|
} else if !synced {
|
|
synced = true
|
|
glog.V(0).Infof("synced with %s", peer)
|
|
}
|
|
lastPersistTime = time.Now()
|
|
counter = 0
|
|
} else {
|
|
glog.V(0).Infof("failed to update offset for %v: %v", peer, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
processEventFn := func(event *filer_pb.SubscribeMetadataResponse) error {
|
|
data, err := proto.Marshal(event)
|
|
if err != nil {
|
|
glog.Errorf("failed to marshal subscribed filer_pb.SubscribeMetadataResponse %+v: %v", event, err)
|
|
return err
|
|
}
|
|
dir := event.Directory
|
|
// println("received meta change", dir, "size", len(data))
|
|
if err := ma.MetaLogBuffer.AddDataToBuffer([]byte(dir), data, event.TsNs); err != nil {
|
|
glog.Errorf("failed to add data to log buffer for %s: %v", dir, err)
|
|
return err
|
|
}
|
|
if maybeReplicateMetadataChange != nil {
|
|
if err := maybeReplicateMetadataChange(event); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The stream will deliver everything after lastTsNs, so the aggregated
|
|
// buffer is (still) complete for this peer up to that point.
|
|
ma.advancePeerWatermark(peer, lastTsNs)
|
|
|
|
glog.V(0).Infof("subscribing remote %s meta change: %v, clientId:%d", peer, time.Unix(0, lastTsNs), ma.filer.UniqueFilerId)
|
|
err = pb.WithFilerClient(true, 0, peer, ma.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
// ctx is cancelled on stopChan by the watch above; the blocking Recv
|
|
// below stops promptly on removal or shutdown.
|
|
atomic.AddInt32(&ma.filer.UniqueFilerEpoch, 1)
|
|
// Construct a log file reader that reads chunks via the peer filer's LookupVolume.
|
|
lookupFn := LookupFn(filerClient{client})
|
|
logFileReaderFn := func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error) {
|
|
return NewChunkStreamReaderFromLookup(ctx, lookupFn, chunks), nil
|
|
}
|
|
|
|
stream, err := client.SubscribeLocalMetadata(ctx, &filer_pb.SubscribeMetadataRequest{
|
|
ClientName: "filer:" + string(self),
|
|
PathPrefix: "/",
|
|
SinceNs: lastTsNs,
|
|
ClientId: ma.filer.UniqueFilerId,
|
|
ClientEpoch: atomic.LoadInt32(&ma.filer.UniqueFilerEpoch),
|
|
ClientSupportsBatching: true,
|
|
ClientSupportsMetadataChunks: true,
|
|
// Idle heartbeats keep a quiet peer from freezing the low-watermark.
|
|
ClientSupportsIdleHeartbeat: true,
|
|
})
|
|
if err != nil {
|
|
glog.V(0).Infof("SubscribeLocalMetadata %v: %v", peer, err)
|
|
return fmt.Errorf("subscribe: %w", err)
|
|
}
|
|
|
|
processOne := func(event *filer_pb.SubscribeMetadataResponse) error {
|
|
// Bail promptly on stop; a cancelled stream may still deliver
|
|
// queued events while unwinding, and replaying them now only
|
|
// delays the offset save the shutdown is waiting on.
|
|
select {
|
|
case <-stopChan:
|
|
return errPeerSubscriptionStopped
|
|
default:
|
|
}
|
|
if err := processEventFn(event); err != nil {
|
|
glog.V(0).Infof("SubscribeLocalMetadata process %v: %v", event, err)
|
|
return fmt.Errorf("process %v: %w", event, err)
|
|
}
|
|
f.onMetadataChangeEvent(event)
|
|
lastTsNs = event.TsNs
|
|
ma.advancePeerWatermark(peer, event.TsNs)
|
|
return nil
|
|
}
|
|
|
|
var pendingRefs []*filer_pb.LogFileChunkRef
|
|
|
|
for {
|
|
resp, listenErr := stream.Recv()
|
|
if listenErr == io.EOF {
|
|
return nil
|
|
}
|
|
if listenErr != nil {
|
|
glog.V(0).Infof("SubscribeLocalMetadata stream %v: %v", peer, listenErr)
|
|
return listenErr
|
|
}
|
|
|
|
// Accumulate log file chunk references
|
|
if len(resp.LogFileRefs) > 0 {
|
|
pendingRefs = append(pendingRefs, resp.LogFileRefs...)
|
|
continue
|
|
}
|
|
|
|
// Process accumulated refs (transition from disk to in-memory)
|
|
if len(pendingRefs) > 0 {
|
|
lastTs, readErr := pb.ReadLogFileRefs(pendingRefs, logFileReaderFn,
|
|
lastTsNs, 0, pb.PathFilter{PathPrefix: "/"},
|
|
func(event *filer_pb.SubscribeMetadataResponse) error {
|
|
return processOne(event)
|
|
})
|
|
if readErr != nil {
|
|
return fmt.Errorf("%w from %s: %w", pb.ErrLogFileRead, peer, readErr)
|
|
}
|
|
if lastTs > 0 {
|
|
lastTsNs = lastTs
|
|
}
|
|
pendingRefs = nil
|
|
}
|
|
|
|
if resp.EventNotification != nil {
|
|
if err := processOne(resp); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Batched events, with the envelope's nil guard mirrored. A nested
|
|
// control message still carries watermark state - dropping it
|
|
// would make healthy flush progress look stalled.
|
|
for _, batchedEvent := range resp.Events {
|
|
if batchedEvent.FlushedTsNs > 0 {
|
|
ma.advancePeerFlushWatermark(peer, batchedEvent.FlushedTsNs)
|
|
}
|
|
if batchedEvent.EventNotification == nil {
|
|
if batchedEvent.TsNs > 0 {
|
|
ma.advancePeerWatermark(peer, batchedEvent.TsNs)
|
|
}
|
|
continue
|
|
}
|
|
if err := processOne(batchedEvent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Idle heartbeat: advance only the watermark, not lastTsNs, so a
|
|
// reconnect still resumes from the last real event.
|
|
if resp.EventNotification == nil && len(resp.Events) == 0 && resp.TsNs > 0 {
|
|
ma.advancePeerWatermark(peer, resp.TsNs)
|
|
}
|
|
if resp.FlushedTsNs > 0 {
|
|
ma.advancePeerFlushWatermark(peer, resp.FlushedTsNs)
|
|
}
|
|
}
|
|
})
|
|
return persistableTsNs(lastTsNs), err
|
|
}
|
|
|
|
// replicateMetadataChange retries transient Replay failures with bounded
|
|
// backoff and reports the outcome. errPeerSubscriptionStopped is returned
|
|
// when the subscription stops mid-retry so the caller aborts and the offset
|
|
// save keeps the last completed event. A failure that outlives the retry
|
|
// budget is counted and logged, then returned: the caller pins the peer's
|
|
// offset at the event so a restart replays it rather than skipping it.
|
|
func replicateMetadataChange(f *Filer, peer pb.ServerAddress, event *filer_pb.SubscribeMetadataResponse, stopChan <-chan struct{}) error {
|
|
waitTime := time.Second
|
|
var err error
|
|
for waitTime < util.RetryWaitTime {
|
|
select {
|
|
case <-stopChan:
|
|
return errPeerSubscriptionStopped
|
|
default:
|
|
}
|
|
if err = f.Replay(event); err == nil {
|
|
return nil
|
|
}
|
|
if !util.IsTransientError(err) {
|
|
break
|
|
}
|
|
glog.V(0).Infof("retry replicate metadata change from %s: %v", peer, err)
|
|
select {
|
|
case <-stopChan:
|
|
return errPeerSubscriptionStopped
|
|
case <-time.After(waitTime):
|
|
}
|
|
waitTime += waitTime / 2
|
|
}
|
|
stats.FilerMetaAggregatorReplayFailures.WithLabelValues(string(peer)).Inc()
|
|
name := event.GetEventNotification().GetNewEntry().GetName()
|
|
if name == "" {
|
|
name = event.GetEventNotification().GetOldEntry().GetName()
|
|
}
|
|
glog.Errorf("giving up replicating metadata change from %s for %s/%s (ts=%d): %v", peer, event.Directory, name, event.TsNs, err)
|
|
return err
|
|
}
|
|
|
|
// errPeerSubscriptionStopped ends event processing once the subscription's
|
|
// stop channel closed.
|
|
var errPeerSubscriptionStopped = errors.New("peer subscription stopped")
|
|
|
|
// traversePeerMetadata does a full BFS traversal of a peer filer's metadata
|
|
// and inserts all entries into the local store. This is used when a filer
|
|
// connects to a peer for the first time and needs to bootstrap pre-existing data.
|
|
// The ctx cancels on the peer's stop channel so a shutdown does not have to
|
|
// out-wait a long traversal.
|
|
func (ma *MetaAggregator) traversePeerMetadata(ctx context.Context, f *Filer, peer pb.ServerAddress) error {
|
|
return pb.WithFilerClient(true, 0, peer, ma.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
stream, err := client.TraverseBfsMetadata(ctx, &filer_pb.TraverseBfsMetadataRequest{
|
|
Directory: "/",
|
|
ExcludedPrefixes: []string{SystemLogDir},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("traverse bfs metadata: %w", err)
|
|
}
|
|
var count int64
|
|
for {
|
|
resp, recvErr := stream.Recv()
|
|
if recvErr == io.EOF {
|
|
break
|
|
}
|
|
if recvErr != nil {
|
|
return fmt.Errorf("traverse bfs metadata recv: %w", recvErr)
|
|
}
|
|
if resp.Entry == nil {
|
|
continue
|
|
}
|
|
entry := FromPbEntry(resp.Directory, resp.Entry)
|
|
// Order on the sender's commit timestamp, not client-settable
|
|
// mtime. Peers predating the field send 0: fall back to mtime.
|
|
peerTsNs := resp.EntryTsNs
|
|
if peerTsNs == 0 {
|
|
peerTsNs = entry.Attr.Mtime.UnixNano()
|
|
}
|
|
if applyErr := f.applyTraversedEntry(ctx, entry, peerTsNs); applyErr != nil {
|
|
return applyErr
|
|
}
|
|
count++
|
|
if count%10000 == 0 {
|
|
glog.V(0).Infof("synced %d entries from peer %s", count, peer)
|
|
}
|
|
}
|
|
glog.V(0).Infof("synced %d entries total from peer %s", count, peer)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// applyTraversedEntry merges one bootstrap entry under the path's replay
|
|
// locks, keeping local state when its commit is newer than the sender's.
|
|
// An applied entry keeps the sender's commit time on its stamp so a later
|
|
// peer replay orders against it rather than the entry's mtime.
|
|
func (f *Filer) applyTraversedEntry(ctx context.Context, entry *Entry, peerTsNs int64) error {
|
|
unlock := f.lockReplayPaths(entry.FullPath, "")
|
|
defer unlock()
|
|
localTsNs, err := f.localCommitTsNs(ctx, entry.FullPath)
|
|
if err != nil {
|
|
return fmt.Errorf("check local state for %s: %w", entry.FullPath, err)
|
|
}
|
|
if localTsNs >= peerTsNs {
|
|
if localTsNs != 0 {
|
|
return nil
|
|
}
|
|
// Zero timestamps carry no order: keep the path only when an entry
|
|
// exists locally — a missing one takes the peer's copy, or an
|
|
// epoch-dated entry from a peer reporting no commit time is dropped.
|
|
if f.Store == nil {
|
|
return nil
|
|
}
|
|
local, findErr := f.Store.FindEntry(ctx, entry.FullPath)
|
|
if findErr != nil && !errors.Is(findErr, filer_pb.ErrNotFound) {
|
|
return fmt.Errorf("check local state for %s: %w", entry.FullPath, findErr)
|
|
}
|
|
if local != nil {
|
|
return nil
|
|
}
|
|
}
|
|
entry.CommitTsNs = peerTsNs
|
|
if insertErr := f.Store.InsertEntry(ctx, entry); insertErr != nil {
|
|
// Entry already exists (root dir, or partial previous bootstrap);
|
|
// the lock kept the check above current, so the peer commit is newer.
|
|
if _, findErr := f.Store.FindEntry(ctx, entry.FullPath); findErr != nil {
|
|
return fmt.Errorf("insert entry %s: %w", entry.FullPath, insertErr)
|
|
}
|
|
if updateErr := f.Store.UpdateEntry(ctx, entry); updateErr != nil {
|
|
return fmt.Errorf("update entry %s: %w", entry.FullPath, updateErr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ma *MetaAggregator) readFilerStoreSignature(ctx context.Context, peer pb.ServerAddress) (sig int32, err error) {
|
|
err = pb.WithFilerClient(false, 0, peer, ma.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
resp, err := client.GetFilerConfiguration(ctx, &filer_pb.GetFilerConfigurationRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sig = resp.Signature
|
|
return nil
|
|
})
|
|
return
|
|
}
|
|
|
|
const (
|
|
MetaOffsetPrefix = "Meta"
|
|
)
|
|
|
|
func GetPeerMetaOffsetKey(peerSignature int32) []byte {
|
|
key := []byte(MetaOffsetPrefix + "xxxx")
|
|
util.Uint32toBytes(key[len(MetaOffsetPrefix):], uint32(peerSignature))
|
|
return key
|
|
}
|
|
|
|
func (ma *MetaAggregator) readOffset(f *Filer, peer pb.ServerAddress, peerSignature int32) (lastTsNs int64, err error) {
|
|
|
|
key := GetPeerMetaOffsetKey(peerSignature)
|
|
|
|
value, err := f.Store.KvGet(context.Background(), key)
|
|
|
|
if err != nil {
|
|
return 0, fmt.Errorf("readOffset %s : %w", peer, err)
|
|
}
|
|
|
|
lastTsNs = int64(util.BytesToUint64(value))
|
|
|
|
glog.V(0).Infof("readOffset %s : %d", peer, lastTsNs)
|
|
|
|
return
|
|
}
|
|
|
|
func (ma *MetaAggregator) updateOffset(f *Filer, peer pb.ServerAddress, peerSignature int32, lastTsNs int64) (err error) {
|
|
|
|
key := GetPeerMetaOffsetKey(peerSignature)
|
|
|
|
value := make([]byte, 8)
|
|
util.Uint64toBytes(value, uint64(lastTsNs))
|
|
|
|
err = f.Store.KvPut(context.Background(), key, value)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("updateOffset %s : %v", peer, err)
|
|
}
|
|
|
|
glog.V(4).Infof("updateOffset %s : %d", peer, lastTsNs)
|
|
|
|
return
|
|
}
|
|
|
|
// filerClient adapts a SeaweedFilerClient to the FilerClient interface
|
|
// for use with LookupFn. Used by MetaAggregator to resolve volume IDs
|
|
// on peer filers.
|
|
type filerClient struct {
|
|
client filer_pb.SeaweedFilerClient
|
|
}
|
|
|
|
func (fc filerClient) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
|
return fn(fc.client)
|
|
}
|
|
|
|
func (fc filerClient) AdjustedUrl(location *filer_pb.Location) string {
|
|
return location.Url
|
|
}
|
|
|
|
func (fc filerClient) GetDataCenter() string {
|
|
return ""
|
|
}
|