blob: 9799c323d7fe410b8790616b698792848e24a9d8 [file]
// Copyright 2025 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cache
import (
"context"
"errors"
"sync"
"time"
"go.etcd.io/etcd/api/v3/etcdserverpb"
clientv3 "go.etcd.io/etcd/client/v3"
)
type demux struct {
mu sync.RWMutex
// activeWatchers & laggingWatchers hold the first revision the watcher still needs (nextRev).
activeWatchers map[*watcher]int64
laggingWatchers map[*watcher]int64
resyncInterval time.Duration
// Range of revisions maintained for demux operations, inclusive. Broader than history as event revision is not contious.
// maxRev tracks highest seen revision; minRev sets watcher compaction threshold (updated to evictedRev+1 on history overflow)
minRev, maxRev int64
// History stores events within [minRev, maxRev].
history ringBuffer[[]*clientv3.Event]
// resynced is used to notify that resync loop was completed.
resynced *notifier
}
func NewDemux(ctx context.Context, wg *sync.WaitGroup, historyWindowSize int, resyncInterval time.Duration) *demux {
d := newDemux(historyWindowSize, resyncInterval)
wg.Add(1)
go func() {
defer wg.Done()
d.resyncLoop(ctx)
}()
return d
}
func newDemux(historyWindowSize int, resyncInterval time.Duration) *demux {
return &demux{
activeWatchers: make(map[*watcher]int64),
laggingWatchers: make(map[*watcher]int64),
history: *newRingBuffer(historyWindowSize, func(batch []*clientv3.Event) int64 { return batch[0].Kv.ModRevision }),
resyncInterval: resyncInterval,
resynced: newNotifier(),
}
}
// resyncLoop periodically tries to catch lagging watchers up by replaying events from History.
func (d *demux) resyncLoop(ctx context.Context) {
timer := time.NewTimer(d.resyncInterval)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
d.resyncLaggingWatchers()
d.resynced.notify()
timer.Reset(d.resyncInterval)
}
}
}
// WaitForNextResync blocks until the next resync loop iteration is complete.
func (d *demux) WaitForNextResync(ctx context.Context) error {
return d.resynced.wait(ctx)
}
func (d *demux) Register(w *watcher, startingRev int64) {
d.mu.Lock()
defer d.mu.Unlock()
if d.maxRev == 0 {
if startingRev == 0 {
d.activeWatchers[w] = 0
} else {
d.laggingWatchers[w] = startingRev
}
return
}
// Special case: 0 means “newest”.
if startingRev == 0 {
startingRev = d.maxRev + 1
}
if startingRev <= d.maxRev {
d.laggingWatchers[w] = startingRev
} else {
d.activeWatchers[w] = startingRev
}
}
func (d *demux) Unregister(w *watcher) {
func() {
d.mu.Lock()
defer d.mu.Unlock()
delete(d.activeWatchers, w)
delete(d.laggingWatchers, w)
}()
w.Stop()
}
func (d *demux) Init(minRev int64) {
d.mu.Lock()
defer d.mu.Unlock()
if minRev == 0 {
return
}
if d.minRev == 0 {
// Watch started for empty demux
d.minRev = minRev
return
}
if d.maxRev == 0 {
// Watch started on initialized demux that never got any event.
d.purge()
d.minRev = minRev
return
}
if minRev == d.maxRev+1 {
// Watch continuing from last revision it observed.
return
}
// Watch opened on revision mismatching dmux last observed revision.
d.purge()
d.minRev = minRev
}
func (d *demux) Broadcast(resp clientv3.WatchResponse) error {
d.mu.Lock()
defer d.mu.Unlock()
if d.minRev == 0 {
return errors.New("demux: not initialized")
}
err := validateRevisions(resp, d.maxRev)
if err != nil {
return err
}
d.updateStoreLocked(resp)
d.broadcastLocked(resp)
return nil
}
func (d *demux) LatestRev() int64 {
d.mu.RLock()
defer d.mu.RUnlock()
return d.maxRev
}
func (d *demux) BroadcastProgress() {
d.mu.Lock()
defer d.mu.Unlock()
if d.maxRev == 0 {
return
}
resp := clientv3.WatchResponse{
Header: &etcdserverpb.ResponseHeader{
Revision: d.maxRev,
},
}
for w := range d.activeWatchers {
w.enqueueResponse(resp)
}
}
func (d *demux) BroadcastProgressTo(w *watcher) {
d.mu.RLock()
defer d.mu.RUnlock()
if d.maxRev == 0 {
return
}
if _, active := d.activeWatchers[w]; !active {
return
}
resp := clientv3.WatchResponse{
Header: &etcdserverpb.ResponseHeader{
Revision: d.maxRev,
},
}
w.enqueueResponse(resp)
}
func (d *demux) updateStoreLocked(resp clientv3.WatchResponse) {
if resp.IsProgressNotify() {
d.maxRev = resp.Header.Revision
return
}
if len(resp.Events) == 0 {
return
}
events := resp.Events
batchStart := 0
for end := 1; end < len(events); end++ {
if events[end].Kv.ModRevision != events[batchStart].Kv.ModRevision {
if end > batchStart {
if end+1 == len(events) && d.history.full() {
d.minRev = d.history.PeekOldest() + 1
}
d.history.Append(events[batchStart:end])
}
batchStart = end
}
}
if batchStart < len(events) {
if d.history.full() {
d.minRev = d.history.PeekOldest() + 1
}
d.history.Append(events[batchStart:])
}
d.maxRev = events[len(events)-1].Kv.ModRevision
}
func (d *demux) broadcastLocked(resp clientv3.WatchResponse) {
switch {
case resp.IsProgressNotify():
d.broadcastProgressLocked(resp.Header.Revision)
case len(resp.Events) != 0:
d.broadcastEventsLocked(resp.Events)
default:
}
}
func (d *demux) broadcastProgressLocked(progressRev int64) {
for w, nextRev := range d.activeWatchers {
if nextRev >= progressRev {
continue
}
resp := clientv3.WatchResponse{
Header: &etcdserverpb.ResponseHeader{
Revision: progressRev,
},
}
if w.enqueueResponse(resp) {
d.activeWatchers[w] = progressRev + 1
}
}
}
func (d *demux) broadcastEventsLocked(events []*clientv3.Event) {
firstRev := events[0].Kv.ModRevision
lastRev := events[len(events)-1].Kv.ModRevision
for w, nextRev := range d.activeWatchers {
if nextRev != 0 && firstRev > nextRev {
d.laggingWatchers[w] = nextRev
delete(d.activeWatchers, w)
continue
}
sendStart := len(events)
for i, ev := range events {
if ev.Kv.ModRevision >= nextRev {
sendStart = i
break
}
}
if sendStart == len(events) {
continue
}
if !w.enqueueResponse(clientv3.WatchResponse{
Events: events[sendStart:],
}) { // overflow → lagging
d.laggingWatchers[w] = nextRev
delete(d.activeWatchers, w)
} else {
d.activeWatchers[w] = lastRev + 1
}
}
}
// Purge stops all watchers and rebase history on watch errors
func (d *demux) Purge() {
d.mu.Lock()
defer d.mu.Unlock()
d.purge()
}
func (d *demux) purge() {
d.maxRev = 0
d.minRev = 0
d.history.RebaseHistory()
for w := range d.activeWatchers {
w.Stop()
}
for w := range d.laggingWatchers {
w.Stop()
}
d.activeWatchers = make(map[*watcher]int64)
d.laggingWatchers = make(map[*watcher]int64)
}
// Compact is called when etcd reports a compaction at compactRev to rebase history;
// it keeps provably-too-old watchers for later cancellation, stops others, and clients should resubscribe.
func (d *demux) Compact(compactRev int64) {
d.mu.Lock()
defer d.mu.Unlock()
d.purge()
}
func (d *demux) resyncLaggingWatchers() {
d.mu.Lock()
defer d.mu.Unlock()
if d.minRev == 0 {
return
}
for w, nextRev := range d.laggingWatchers {
if nextRev < d.minRev {
w.Compact(nextRev)
delete(d.laggingWatchers, w)
continue
}
// TODO: re-enable key‐predicate in Filter when non‐zero startRev or performance tuning is needed
resyncSuccess := true
d.history.AscendGreaterOrEqual(nextRev, func(rev int64, eventBatch []*clientv3.Event) bool {
resp := clientv3.WatchResponse{
Events: eventBatch,
}
if !w.enqueueResponse(resp) { // buffer overflow: watcher still lagging
resyncSuccess = false
return false
}
nextRev = rev + 1
return true
})
// Send progress to just resync.
if resyncSuccess {
resp := clientv3.WatchResponse{
Header: &etcdserverpb.ResponseHeader{Revision: d.maxRev},
}
if d.maxRev > nextRev && w.enqueueResponse(resp) {
nextRev = d.maxRev + 1
}
delete(d.laggingWatchers, w)
d.activeWatchers[w] = nextRev
} else {
d.laggingWatchers[w] = nextRev
}
}
}
func newNotifier() *notifier {
return &notifier{
ch: make(chan struct{}),
}
}
type notifier struct {
mu sync.RWMutex
ch chan struct{}
}
func (n *notifier) notify() {
n.mu.Lock()
defer n.mu.Unlock()
previous := n.ch
n.ch = make(chan struct{})
close(previous)
}
func (n *notifier) wait(ctx context.Context) error {
n.mu.RLock()
ch := n.ch
n.mu.RUnlock()
select {
case <-ch:
return nil
case <-ctx.Done():
return ctx.Err()
}
}