mirror of
https://github.com/ceph/ceph-csi.git
synced 2025-06-14 02:43:36 +00:00
rebase: update kubernetes to 1.30
updating kubernetes to 1.30 release Signed-off-by: Madhu Rajanna <madhupr007@gmail.com>
This commit is contained in:
committed by
mergify[bot]
parent
62ddcf715b
commit
e727bd351e
104
vendor/k8s.io/apiserver/pkg/storage/cacher/cacher.go
generated
vendored
104
vendor/k8s.io/apiserver/pkg/storage/cacher/cacher.go
generated
vendored
@ -45,9 +45,9 @@ import (
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/component-base/tracing"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/clock"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -422,7 +422,7 @@ func NewCacherFromConfig(config Config) (*Cacher, error) {
|
||||
reflector.MaxInternalErrorRetryDuration = time.Second * 30
|
||||
// since the watch-list is provided by the watch cache instruct
|
||||
// the reflector to issue a regular LIST against the store
|
||||
reflector.UseWatchList = false
|
||||
reflector.UseWatchList = ptr.To(false)
|
||||
|
||||
cacher.watchCache = watchCache
|
||||
cacher.reflector = reflector
|
||||
@ -522,7 +522,8 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.WatchList) && opts.SendInitialEvents != nil {
|
||||
opts.SendInitialEvents = nil
|
||||
}
|
||||
if opts.SendInitialEvents == nil && opts.ResourceVersion == "" {
|
||||
// TODO: we should eventually get rid of this legacy case
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.WatchFromStorageWithoutResourceVersion) && opts.SendInitialEvents == nil && opts.ResourceVersion == "" {
|
||||
return c.storage.Watch(ctx, key, opts)
|
||||
}
|
||||
requestedWatchRV, err := c.versioner.ParseResourceVersion(opts.ResourceVersion)
|
||||
@ -554,6 +555,7 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions
|
||||
if field == c.indexedTrigger.indexName {
|
||||
if value, ok := pred.Field.RequiresExactMatch(field); ok {
|
||||
triggerValue, triggerSupported = value, true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -566,14 +568,14 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions
|
||||
// watchers on our watcher having a processing hiccup
|
||||
chanSize := c.watchCache.suggestedWatchChannelSize(c.indexedTrigger != nil, triggerSupported)
|
||||
|
||||
// Determine a function that computes the bookmarkAfterResourceVersion
|
||||
bookmarkAfterResourceVersionFn, err := c.getBookmarkAfterResourceVersionLockedFunc(ctx, requestedWatchRV, opts)
|
||||
// Determine the ResourceVersion to which the watch cache must be synchronized
|
||||
requiredResourceVersion, err := c.getWatchCacheResourceVersion(ctx, requestedWatchRV, opts)
|
||||
if err != nil {
|
||||
return newErrWatcher(err), nil
|
||||
}
|
||||
|
||||
// Determine a function that computes the watchRV we should start from
|
||||
startWatchResourceVersionFn, err := c.getStartResourceVersionForWatchLockedFunc(ctx, requestedWatchRV, opts)
|
||||
// Determine a function that computes the bookmarkAfterResourceVersion
|
||||
bookmarkAfterResourceVersionFn, err := c.getBookmarkAfterResourceVersionLockedFunc(requestedWatchRV, requiredResourceVersion, opts)
|
||||
if err != nil {
|
||||
return newErrWatcher(err), nil
|
||||
}
|
||||
@ -605,7 +607,7 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions
|
||||
// moreover even though the c.waitUntilWatchCacheFreshAndForceAllEvents acquires a lock
|
||||
// it is safe to release the lock after the method finishes because we don't require
|
||||
// any atomicity between the call to the method and further calls that actually get the events.
|
||||
forceAllEvents, err := c.waitUntilWatchCacheFreshAndForceAllEvents(ctx, requestedWatchRV, opts)
|
||||
err = c.waitUntilWatchCacheFreshAndForceAllEvents(ctx, requiredResourceVersion, opts)
|
||||
if err != nil {
|
||||
return newErrWatcher(err), nil
|
||||
}
|
||||
@ -618,13 +620,8 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions
|
||||
c.watchCache.RLock()
|
||||
defer c.watchCache.RUnlock()
|
||||
|
||||
startWatchRV := startWatchResourceVersionFn()
|
||||
var cacheInterval *watchCacheInterval
|
||||
if forceAllEvents {
|
||||
cacheInterval, err = c.watchCache.getIntervalFromStoreLocked()
|
||||
} else {
|
||||
cacheInterval, err = c.watchCache.getAllEventsSinceLocked(startWatchRV)
|
||||
}
|
||||
cacheInterval, err = c.watchCache.getAllEventsSinceLocked(requiredResourceVersion, opts)
|
||||
if err != nil {
|
||||
// To match the uncached watch implementation, once we have passed authn/authz/admission,
|
||||
// and successfully parsed a resource version, other errors must fail with a watch event of type ERROR,
|
||||
@ -666,7 +663,7 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions
|
||||
return newImmediateCloseWatcher(), nil
|
||||
}
|
||||
|
||||
go watcher.processInterval(ctx, cacheInterval, startWatchRV)
|
||||
go watcher.processInterval(ctx, cacheInterval, requiredResourceVersion)
|
||||
return watcher, nil
|
||||
}
|
||||
|
||||
@ -755,7 +752,7 @@ func (c *Cacher) listItems(ctx context.Context, listRV uint64, key string, pred
|
||||
}
|
||||
return nil, readResourceVersion, "", nil
|
||||
}
|
||||
return c.watchCache.WaitUntilFreshAndList(ctx, listRV, pred.MatcherIndex())
|
||||
return c.watchCache.WaitUntilFreshAndList(ctx, listRV, pred.MatcherIndex(ctx))
|
||||
}
|
||||
|
||||
// GetList implements storage.Interface
|
||||
@ -776,7 +773,7 @@ func (c *Cacher) GetList(ctx context.Context, key string, opts storage.ListOptio
|
||||
// minimal resource version, simply forward the request to storage.
|
||||
return c.storage.GetList(ctx, key, opts, listObj)
|
||||
}
|
||||
if listRV == 0 && utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) {
|
||||
if resourceVersion == "" && utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) {
|
||||
listRV, err = storage.GetCurrentResourceVersionFromStorage(ctx, c.storage, c.newListFunc, c.resourcePrefix, c.objectType.String())
|
||||
if err != nil {
|
||||
return err
|
||||
@ -1258,59 +1255,62 @@ func (c *Cacher) LastSyncResourceVersion() (uint64, error) {
|
||||
// spits a ResourceVersion after which the bookmark event will be delivered.
|
||||
//
|
||||
// The returned function must be called under the watchCache lock.
|
||||
func (c *Cacher) getBookmarkAfterResourceVersionLockedFunc(ctx context.Context, parsedResourceVersion uint64, opts storage.ListOptions) (func() uint64, error) {
|
||||
func (c *Cacher) getBookmarkAfterResourceVersionLockedFunc(parsedResourceVersion, requiredResourceVersion uint64, opts storage.ListOptions) (func() uint64, error) {
|
||||
if opts.SendInitialEvents == nil || !*opts.SendInitialEvents || !opts.Predicate.AllowWatchBookmarks {
|
||||
return func() uint64 { return 0 }, nil
|
||||
}
|
||||
return c.getCommonResourceVersionLockedFunc(ctx, parsedResourceVersion, opts)
|
||||
}
|
||||
|
||||
// getStartResourceVersionForWatchLockedFunc returns a function that
|
||||
// spits a ResourceVersion the watch will be started from.
|
||||
// Depending on the input parameters the semantics of the returned ResourceVersion are:
|
||||
// - start at Exact (return parsedWatchResourceVersion)
|
||||
// - start at Most Recent (return an RV from etcd)
|
||||
// - start at Any (return the current watchCache's RV)
|
||||
//
|
||||
// The returned function must be called under the watchCache lock.
|
||||
func (c *Cacher) getStartResourceVersionForWatchLockedFunc(ctx context.Context, parsedWatchResourceVersion uint64, opts storage.ListOptions) (func() uint64, error) {
|
||||
if opts.SendInitialEvents == nil || *opts.SendInitialEvents {
|
||||
return func() uint64 { return parsedWatchResourceVersion }, nil
|
||||
}
|
||||
return c.getCommonResourceVersionLockedFunc(ctx, parsedWatchResourceVersion, opts)
|
||||
}
|
||||
|
||||
// getCommonResourceVersionLockedFunc a helper that simply computes a ResourceVersion
|
||||
// based on the input parameters. Please examine callers of this method to get more context.
|
||||
//
|
||||
// The returned function must be called under the watchCache lock.
|
||||
func (c *Cacher) getCommonResourceVersionLockedFunc(ctx context.Context, parsedWatchResourceVersion uint64, opts storage.ListOptions) (func() uint64, error) {
|
||||
switch {
|
||||
case len(opts.ResourceVersion) == 0:
|
||||
rv, err := storage.GetCurrentResourceVersionFromStorage(ctx, c.storage, c.newListFunc, c.resourcePrefix, c.objectType.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() uint64 { return rv }, nil
|
||||
case parsedWatchResourceVersion == 0:
|
||||
return func() uint64 { return requiredResourceVersion }, nil
|
||||
case parsedResourceVersion == 0:
|
||||
// here we assume that watchCache locked is already held
|
||||
return func() uint64 { return c.watchCache.resourceVersion }, nil
|
||||
default:
|
||||
return func() uint64 { return parsedWatchResourceVersion }, nil
|
||||
return func() uint64 { return parsedResourceVersion }, nil
|
||||
}
|
||||
}
|
||||
|
||||
// getWatchCacheResourceVersion returns a ResourceVersion to which the watch cache must be synchronized to
|
||||
//
|
||||
// Depending on the input parameters, the semantics of the returned ResourceVersion are:
|
||||
// - must be at Exact RV (when parsedWatchResourceVersion > 0)
|
||||
// - can be at Any RV (when parsedWatchResourceVersion = 0)
|
||||
// - must be at Most Recent RV (return an RV from etcd)
|
||||
//
|
||||
// note that the above semantic is enforced by the API validation (defined elsewhere):
|
||||
//
|
||||
// if SendInitiaEvents != nil => ResourceVersionMatch = NotOlderThan
|
||||
// if ResourceVersionmatch != nil => ResourceVersionMatch = NotOlderThan & SendInitialEvents != nil
|
||||
func (c *Cacher) getWatchCacheResourceVersion(ctx context.Context, parsedWatchResourceVersion uint64, opts storage.ListOptions) (uint64, error) {
|
||||
if len(opts.ResourceVersion) != 0 {
|
||||
return parsedWatchResourceVersion, nil
|
||||
}
|
||||
// legacy case
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.WatchFromStorageWithoutResourceVersion) && opts.SendInitialEvents == nil && opts.ResourceVersion == "" {
|
||||
return 0, nil
|
||||
}
|
||||
rv, err := storage.GetCurrentResourceVersionFromStorage(ctx, c.storage, c.newListFunc, c.resourcePrefix, c.objectType.String())
|
||||
return rv, err
|
||||
}
|
||||
|
||||
// waitUntilWatchCacheFreshAndForceAllEvents waits until cache is at least
|
||||
// as fresh as given requestedWatchRV if sendInitialEvents was requested.
|
||||
// Additionally, it instructs the caller whether it should ask for
|
||||
// all events from the cache (full state) or not.
|
||||
func (c *Cacher) waitUntilWatchCacheFreshAndForceAllEvents(ctx context.Context, requestedWatchRV uint64, opts storage.ListOptions) (bool, error) {
|
||||
// otherwise, we allow for establishing the connection because the clients
|
||||
// can wait for events without unnecessary blocking.
|
||||
func (c *Cacher) waitUntilWatchCacheFreshAndForceAllEvents(ctx context.Context, requestedWatchRV uint64, opts storage.ListOptions) error {
|
||||
if opts.SendInitialEvents != nil && *opts.SendInitialEvents {
|
||||
// TODO(p0lyn0mial): adapt the following logic once
|
||||
// https://github.com/kubernetes/kubernetes/pull/123264 merges
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && c.watchCache.notFresh(requestedWatchRV) {
|
||||
c.watchCache.waitingUntilFresh.Add()
|
||||
defer c.watchCache.waitingUntilFresh.Remove()
|
||||
}
|
||||
err := c.watchCache.waitUntilFreshAndBlock(ctx, requestedWatchRV)
|
||||
defer c.watchCache.RUnlock()
|
||||
return err == nil, err
|
||||
return err
|
||||
}
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// errWatcher implements watch.Interface to return a single error
|
||||
|
11
vendor/k8s.io/apiserver/pkg/storage/cacher/metrics/metrics.go
generated
vendored
11
vendor/k8s.io/apiserver/pkg/storage/cacher/metrics/metrics.go
generated
vendored
@ -146,6 +146,16 @@ var (
|
||||
},
|
||||
[]string{"resource"},
|
||||
)
|
||||
|
||||
WatchCacheReadWait = compbasemetrics.NewHistogramVec(
|
||||
&compbasemetrics.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "read_wait_seconds",
|
||||
Help: "Histogram of time spent waiting for a watch cache to become fresh.",
|
||||
StabilityLevel: compbasemetrics.ALPHA,
|
||||
Buckets: []float64{0.005, 0.025, 0.05, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0, 1.25, 1.5, 2, 3},
|
||||
}, []string{"resource"})
|
||||
)
|
||||
|
||||
var registerMetrics sync.Once
|
||||
@ -165,6 +175,7 @@ func Register() {
|
||||
legacyregistry.MustRegister(watchCacheCapacityDecreaseTotal)
|
||||
legacyregistry.MustRegister(WatchCacheCapacity)
|
||||
legacyregistry.MustRegister(WatchCacheInitializations)
|
||||
legacyregistry.MustRegister(WatchCacheReadWait)
|
||||
})
|
||||
}
|
||||
|
||||
|
14
vendor/k8s.io/apiserver/pkg/storage/cacher/util.go
generated
vendored
14
vendor/k8s.io/apiserver/pkg/storage/cacher/util.go
generated
vendored
@ -44,17 +44,3 @@ func hasPathPrefix(s, pathPrefix string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
50
vendor/k8s.io/apiserver/pkg/storage/cacher/watch_cache.go
generated
vendored
50
vendor/k8s.io/apiserver/pkg/storage/cacher/watch_cache.go
generated
vendored
@ -440,6 +440,11 @@ func (w *watchCache) List() []interface{} {
|
||||
// You HAVE TO explicitly call w.RUnlock() after this function.
|
||||
func (w *watchCache) waitUntilFreshAndBlock(ctx context.Context, resourceVersion uint64) error {
|
||||
startTime := w.clock.Now()
|
||||
defer func() {
|
||||
if resourceVersion > 0 {
|
||||
metrics.WatchCacheReadWait.WithContext(ctx).WithLabelValues(w.groupResource.String()).Observe(w.clock.Since(startTime).Seconds())
|
||||
}
|
||||
}()
|
||||
|
||||
// In case resourceVersion is 0, we accept arbitrarily stale result.
|
||||
// As a result, the condition in the below for loop will never be
|
||||
@ -492,8 +497,7 @@ func (s sortableStoreElements) Swap(i, j int) {
|
||||
|
||||
// WaitUntilFreshAndList returns list of pointers to `storeElement` objects along
|
||||
// with their ResourceVersion and the name of the index, if any, that was used.
|
||||
func (w *watchCache) WaitUntilFreshAndList(ctx context.Context, resourceVersion uint64, matchValues []storage.MatchValue) ([]interface{}, uint64, string, error) {
|
||||
var err error
|
||||
func (w *watchCache) WaitUntilFreshAndList(ctx context.Context, resourceVersion uint64, matchValues []storage.MatchValue) (result []interface{}, rv uint64, index string, err error) {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && w.notFresh(resourceVersion) {
|
||||
w.waitingUntilFresh.Add()
|
||||
err = w.waitUntilFreshAndBlock(ctx, resourceVersion)
|
||||
@ -501,12 +505,14 @@ func (w *watchCache) WaitUntilFreshAndList(ctx context.Context, resourceVersion
|
||||
} else {
|
||||
err = w.waitUntilFreshAndBlock(ctx, resourceVersion)
|
||||
}
|
||||
|
||||
defer func() { sort.Sort(sortableStoreElements(result)) }()
|
||||
defer w.RUnlock()
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
return result, rv, index, err
|
||||
}
|
||||
|
||||
result, rv, index, err := func() ([]interface{}, uint64, string, error) {
|
||||
result, rv, index, err = func() ([]interface{}, uint64, string, error) {
|
||||
// This isn't the place where we do "final filtering" - only some "prefiltering" is happening here. So the only
|
||||
// requirement here is to NOT miss anything that should be returned. We can return as many non-matching items as we
|
||||
// want - they will be filtered out later. The fact that we return less things is only further performance improvement.
|
||||
@ -519,7 +525,6 @@ func (w *watchCache) WaitUntilFreshAndList(ctx context.Context, resourceVersion
|
||||
return w.store.List(), w.resourceVersion, "", nil
|
||||
}()
|
||||
|
||||
sort.Sort(sortableStoreElements(result))
|
||||
return result, rv, index, err
|
||||
}
|
||||
|
||||
@ -531,7 +536,14 @@ func (w *watchCache) notFresh(resourceVersion uint64) bool {
|
||||
|
||||
// WaitUntilFreshAndGet returns a pointers to <storeElement> object.
|
||||
func (w *watchCache) WaitUntilFreshAndGet(ctx context.Context, resourceVersion uint64, key string) (interface{}, bool, uint64, error) {
|
||||
err := w.waitUntilFreshAndBlock(ctx, resourceVersion)
|
||||
var err error
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && w.notFresh(resourceVersion) {
|
||||
w.waitingUntilFresh.Add()
|
||||
err = w.waitUntilFreshAndBlock(ctx, resourceVersion)
|
||||
w.waitingUntilFresh.Remove()
|
||||
} else {
|
||||
err = w.waitUntilFreshAndBlock(ctx, resourceVersion)
|
||||
}
|
||||
defer w.RUnlock()
|
||||
if err != nil {
|
||||
return nil, false, 0, err
|
||||
@ -691,7 +703,11 @@ func (w *watchCache) isIndexValidLocked(index int) bool {
|
||||
// getAllEventsSinceLocked returns a watchCacheInterval that can be used to
|
||||
// retrieve events since a certain resourceVersion. This function assumes to
|
||||
// be called under the watchCache lock.
|
||||
func (w *watchCache) getAllEventsSinceLocked(resourceVersion uint64) (*watchCacheInterval, error) {
|
||||
func (w *watchCache) getAllEventsSinceLocked(resourceVersion uint64, opts storage.ListOptions) (*watchCacheInterval, error) {
|
||||
if opts.SendInitialEvents != nil && *opts.SendInitialEvents {
|
||||
return w.getIntervalFromStoreLocked()
|
||||
}
|
||||
|
||||
size := w.endIndex - w.startIndex
|
||||
var oldest uint64
|
||||
switch {
|
||||
@ -711,13 +727,19 @@ func (w *watchCache) getAllEventsSinceLocked(resourceVersion uint64) (*watchCach
|
||||
}
|
||||
|
||||
if resourceVersion == 0 {
|
||||
// resourceVersion = 0 means that we don't require any specific starting point
|
||||
// and we would like to start watching from ~now.
|
||||
// However, to keep backward compatibility, we additionally need to return the
|
||||
// current state and only then start watching from that point.
|
||||
//
|
||||
// TODO: In v2 api, we should stop returning the current state - #13969.
|
||||
return w.getIntervalFromStoreLocked()
|
||||
if opts.SendInitialEvents == nil {
|
||||
// resourceVersion = 0 means that we don't require any specific starting point
|
||||
// and we would like to start watching from ~now.
|
||||
// However, to keep backward compatibility, we additionally need to return the
|
||||
// current state and only then start watching from that point.
|
||||
//
|
||||
// TODO: In v2 api, we should stop returning the current state - #13969.
|
||||
return w.getIntervalFromStoreLocked()
|
||||
}
|
||||
// SendInitialEvents = false and resourceVersion = 0
|
||||
// means that the request would like to start watching
|
||||
// from Any resourceVersion
|
||||
resourceVersion = w.resourceVersion
|
||||
}
|
||||
if resourceVersion < oldest-1 {
|
||||
return nil, errors.NewResourceExpired(fmt.Sprintf("too old resource version: %d (%d)", resourceVersion, oldest-1))
|
||||
|
17
vendor/k8s.io/apiserver/pkg/storage/cacher/watch_cache_interval.go
generated
vendored
17
vendor/k8s.io/apiserver/pkg/storage/cacher/watch_cache_interval.go
generated
vendored
@ -18,6 +18,7 @@ package cacher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
@ -114,9 +115,24 @@ func newCacheInterval(startIndex, endIndex int, indexer indexerFunc, indexValida
|
||||
}
|
||||
}
|
||||
|
||||
type sortableWatchCacheEvents []*watchCacheEvent
|
||||
|
||||
func (s sortableWatchCacheEvents) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s sortableWatchCacheEvents) Less(i, j int) bool {
|
||||
return s[i].Key < s[j].Key
|
||||
}
|
||||
|
||||
func (s sortableWatchCacheEvents) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// newCacheIntervalFromStore is meant to handle the case of rv=0, such that the events
|
||||
// returned by Next() need to be events from a List() done on the underlying store of
|
||||
// the watch cache.
|
||||
// The items returned in the interval will be sorted by Key.
|
||||
func newCacheIntervalFromStore(resourceVersion uint64, store cache.Indexer, getAttrsFunc attrFunc) (*watchCacheInterval, error) {
|
||||
buffer := &watchCacheIntervalBuffer{}
|
||||
allItems := store.List()
|
||||
@ -140,6 +156,7 @@ func newCacheIntervalFromStore(resourceVersion uint64, store cache.Indexer, getA
|
||||
}
|
||||
buffer.endIndex++
|
||||
}
|
||||
sort.Sort(sortableWatchCacheEvents(buffer.buffer))
|
||||
ci := &watchCacheInterval{
|
||||
startIndex: 0,
|
||||
// Simulate that we already have all the events we're looking for.
|
||||
|
Reference in New Issue
Block a user