Repository: etcd-io/etcd
File: cache/watcher.go (Go)

Code before:
// 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 (
	"sync"

	"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
	clientv3 "go.etcd.io/etcd/client/v3"
)

// watcher holds one client’s buffered stream of events.
type watcher struct {
	eventsCh chan clientv3.WatchResponse
	cancelCh chan clientv3.WatchResponse
	keyPred  KeyPredicate
	stopOnce sync.Once
}

func newWatcher(bufSize int, pred KeyPredicate) *watcher {
	return &watcher{
		eventsCh: make(chan clientv3.WatchResponse, bufSize),
		cancelCh: make(chan clientv3.WatchResponse, 1),
		keyPred:  pred,
	}
}

// true  -> events delivered (or filtered/duplicate)
// false -> buffer full (caller should mark watcher “lagging”)
func (w *watcher) enqueueEvent(eventBatch []*clientv3.Event) (enqueued bool) {
	if w.keyPred != nil {
		filtered := make([]*clientv3.Event, 0, len(eventBatch))
		for _, event := range eventBatch {
			if w.keyPred(event.Kv.Key) {
				filtered = append(filtered, event)
			}
		}
		if len(filtered) == 0 {
			return true

Review comment (refactor):
My recommended solution for compaction:
1. Watcher should stop receiving new events
2. Watcher should check if there is a cancel reason/response.
3. Watcher should write the cancel response
4. Watcher should close write channel

So I expect that compaction from demux perspective should work as follows:
1. We set the cancel reason
1. We close the events channel

This means we need 3 variables:
* `respCh chan clientv3.WatchResponse`; note this could be still `chan []*clientv3.Event` and refactor to `chan clientv3.WatchResponse` could be moved to separate PR.
* `cancelResp` *clientv3.WatchResponse.
* `stopOnce sync.Once`

We don't need mutex or channel, as long we guarantee that we set the `cancelResp` before we close `respCh` and do that under `stopOnce` to avoid races with `Stop`.

 

```
func (w *watcher) Stop() {
	w.stopOnce.Do(func() {
		close(w.eventsCh)
	})
}

func (w *watcher) Compact(compactRev int64) bool {
	resp := clientv3.WatchResponse{
		Canceled:        true,
		CompactRevision: compactRev,
		CancelReason:    rpctypes.ErrCompacted.Error(),
	}
	w.stopOnce.Do(func() {
		w.cancelResp = resp
		close(w.eventsCh)
	})
}
```
Watch select is simple:
```
			case resp, ok := <-w.eventsCh:
				if !ok {
					if w.cancelResp != nil {
						select {
							case <-ctx.Done():
							case <-c.internalCtx.Done():
							case responseChan <- w.cancelResp:
						}
					}
					return
				}
```

Code after:
// 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 (
	"sync"

	"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
	clientv3 "go.etcd.io/etcd/client/v3"
)

// watcher holds one client’s buffered stream of events.
type watcher struct {
	eventQueue chan []*clientv3.Event
	cancelResp *clientv3.WatchResponse
	keyPred    KeyPredicate
	stopOnce   sync.Once
}

func newWatcher(bufSize int, pred KeyPredicate) *watcher {
	return &watcher{
		eventQueue: make(chan []*clientv3.Event, bufSize),
		keyPred:    pred,
	}
}

// true  -> events delivered (or filtered/duplicate)
// false -> buffer full (caller should mark watcher “lagging”)
func (w *watcher) enqueueEvent(eventBatch []*clientv3.Event) bool {
	if w.keyPred != nil {
		filtered := make([]*clientv3.Event, 0, len(eventBatch))
		for _, event := range eventBatch {
			if w.keyPred(event.Kv.Key) {
				filtered = append(filtered, event)
			}
		}
		if len(filtered) == 0 {
			return true
		}