mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-11 15:34:56 +00:00
lib/promscrape: follow-up after 91e290a8ff
This commit is contained in:
parent
91e290a8ff
commit
2341bd48d7
4 changed files with 147 additions and 109 deletions
|
@ -1054,7 +1054,7 @@ func getLabelsContext() *labelsContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
func putLabelsContext(lctx *labelsContext) {
|
func putLabelsContext(lctx *labelsContext) {
|
||||||
labels := lctx.labels
|
labels := lctx.labels[:cap(lctx.labels)]
|
||||||
for i := range labels {
|
for i := range labels {
|
||||||
labels[i].Name = ""
|
labels[i].Name = ""
|
||||||
labels[i].Value = ""
|
labels[i].Value = ""
|
||||||
|
@ -1222,27 +1222,24 @@ func internLabelStrings(labels []prompbmarshal.Label) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func internString(s string) string {
|
func internString(s string) string {
|
||||||
if sInterned, ok := internStringsMap.Load(s); ok {
|
if v, ok := internStringsMap.Load(s); ok {
|
||||||
return sInterned.(string)
|
sp := v.(*string)
|
||||||
}
|
return *sp
|
||||||
isc := atomic.LoadUint64(&internStringCount)
|
|
||||||
if isc > 100e3 {
|
|
||||||
internStringsMapLock.Lock()
|
|
||||||
internStringsMap = sync.Map{}
|
|
||||||
atomic.AddUint64(&internStringCount, ^(isc - 1))
|
|
||||||
internStringsMapLock.Unlock()
|
|
||||||
}
|
}
|
||||||
// Make a new copy for s in order to remove references from possible bigger string s refers to.
|
// Make a new copy for s in order to remove references from possible bigger string s refers to.
|
||||||
sCopy := string(append([]byte{}, s...))
|
sCopy := string(append([]byte{}, s...))
|
||||||
internStringsMap.Store(sCopy, sCopy)
|
internStringsMap.Store(sCopy, &sCopy)
|
||||||
atomic.AddUint64(&internStringCount, 1)
|
n := atomic.AddUint64(&internStringsMapLen, 1)
|
||||||
|
if n > 100e3 {
|
||||||
|
atomic.StoreUint64(&internStringsMapLen, 0)
|
||||||
|
internStringsMap = &sync.Map{}
|
||||||
|
}
|
||||||
return sCopy
|
return sCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
internStringCount = uint64(0)
|
internStringsMap = &sync.Map{}
|
||||||
internStringsMapLock sync.Mutex
|
internStringsMapLen uint64
|
||||||
internStringsMap = sync.Map{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getParamsFromLabels(labels []prompbmarshal.Label, paramsOrig map[string][]string) map[string][]string {
|
func getParamsFromLabels(labels []prompbmarshal.Label, paramsOrig map[string][]string) map[string][]string {
|
||||||
|
|
|
@ -14,6 +14,44 @@ import (
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestInternStringSerial(t *testing.T) {
|
||||||
|
if err := testInternString(t); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternStringConcurrent(t *testing.T) {
|
||||||
|
concurrency := 5
|
||||||
|
resultCh := make(chan error, concurrency)
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
go func() {
|
||||||
|
resultCh <- testInternString(t)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(5*time.Second)
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
select {
|
||||||
|
case err := <-resultCh:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
case <-timer.C:
|
||||||
|
t.Fatalf("timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInternString(t *testing.T) error {
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
s := fmt.Sprintf("foo_%d", i)
|
||||||
|
s1 := internString(s)
|
||||||
|
if s != s1 {
|
||||||
|
return fmt.Errorf("unexpected string returned from internString; got %q; want %q", s1, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestMergeLabels(t *testing.T) {
|
func TestMergeLabels(t *testing.T) {
|
||||||
f := func(swc *scrapeWorkConfig, target string, extraLabels, metaLabels map[string]string, resultExpected string) {
|
f := func(swc *scrapeWorkConfig, target string, extraLabels, metaLabels map[string]string, resultExpected string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
25
lib/promscrape/config_timing_test.go
Normal file
25
lib/promscrape/config_timing_test.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package promscrape
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkInternString(b *testing.B) {
|
||||||
|
a := make([]string, 10000)
|
||||||
|
for i := range a {
|
||||||
|
a[i] = fmt.Sprintf("string_%d", i)
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(a)))
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
for _, s := range a {
|
||||||
|
sResult := internString(s)
|
||||||
|
if sResult != s {
|
||||||
|
panic(fmt.Sprintf("unexpected string obtained; got %q; want %q", sResult, s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -108,7 +108,7 @@ func (aw *apiWatcher) reloadScrapeWorks(uw *urlWatcher, swosByKey map[string][]i
|
||||||
}
|
}
|
||||||
|
|
||||||
func (aw *apiWatcher) setScrapeWorks(uw *urlWatcher, key string, labels []map[string]string) {
|
func (aw *apiWatcher) setScrapeWorks(uw *urlWatcher, key string, labels []map[string]string) {
|
||||||
swos := aw.getScrapeWorkObjectsForLabels(labels)
|
swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels)
|
||||||
aw.swosByURLWatcherLock.Lock()
|
aw.swosByURLWatcherLock.Lock()
|
||||||
swosByKey := aw.swosByURLWatcher[uw]
|
swosByKey := aw.swosByURLWatcher[uw]
|
||||||
if swosByKey == nil {
|
if swosByKey == nil {
|
||||||
|
@ -134,10 +134,10 @@ func (aw *apiWatcher) removeScrapeWorks(uw *urlWatcher, key string) {
|
||||||
aw.swosByURLWatcherLock.Unlock()
|
aw.swosByURLWatcherLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (aw *apiWatcher) getScrapeWorkObjectsForLabels(labelss []map[string]string) []interface{} {
|
func getScrapeWorkObjectsForLabels(swcFunc ScrapeWorkConstructorFunc, labelss []map[string]string) []interface{} {
|
||||||
swos := make([]interface{}, 0, len(labelss))
|
swos := make([]interface{}, 0, len(labelss))
|
||||||
for _, labels := range labelss {
|
for _, labels := range labelss {
|
||||||
swo := aw.swcFunc(labels)
|
swo := swcFunc(labels)
|
||||||
// The reflect check is needed because of https://mangatmodi.medium.com/go-check-nil-interface-the-right-way-d142776edef1
|
// The reflect check is needed because of https://mangatmodi.medium.com/go-check-nil-interface-the-right-way-d142776edef1
|
||||||
if swo != nil && !reflect.ValueOf(swo).IsNil() {
|
if swo != nil && !reflect.ValueOf(swo).IsNil() {
|
||||||
swos = append(swos, swo)
|
swos = append(swos, swo)
|
||||||
|
@ -150,21 +150,14 @@ func (aw *apiWatcher) getScrapeWorkObjectsForLabels(labelss []map[string]string)
|
||||||
func (aw *apiWatcher) getScrapeWorkObjects() []interface{} {
|
func (aw *apiWatcher) getScrapeWorkObjects() []interface{} {
|
||||||
aw.gw.registerPendingAPIWatchers()
|
aw.gw.registerPendingAPIWatchers()
|
||||||
|
|
||||||
|
swos := make([]interface{}, 0, aw.swosCount.Get())
|
||||||
aw.swosByURLWatcherLock.Lock()
|
aw.swosByURLWatcherLock.Lock()
|
||||||
defer aw.swosByURLWatcherLock.Unlock()
|
|
||||||
|
|
||||||
size := 0
|
|
||||||
for _, swosByKey := range aw.swosByURLWatcher {
|
|
||||||
for _, swosLocal := range swosByKey {
|
|
||||||
size += len(swosLocal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
swos := make([]interface{}, 0, size)
|
|
||||||
for _, swosByKey := range aw.swosByURLWatcher {
|
for _, swosByKey := range aw.swosByURLWatcher {
|
||||||
for _, swosLocal := range swosByKey {
|
for _, swosLocal := range swosByKey {
|
||||||
swos = append(swos, swosLocal...)
|
swos = append(swos, swosLocal...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
aw.swosByURLWatcherLock.Unlock()
|
||||||
return swos
|
return swos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,18 +289,6 @@ func (gw *groupWatcher) startWatchersForRole(role string, aw *apiWatcher) {
|
||||||
if needStart {
|
if needStart {
|
||||||
uw.reloadObjects()
|
uw.reloadObjects()
|
||||||
go uw.watchForUpdates()
|
go uw.watchForUpdates()
|
||||||
if role == "endpoints" || role == "endpointslice" {
|
|
||||||
// Refresh endpoints and enpointslices targets in background, since they depend on other object types such as pod and service.
|
|
||||||
// This should fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240 .
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
gw.mu.Lock()
|
|
||||||
uw.reloadScrapeWorksForAPIWatchersLocked(uw.aws)
|
|
||||||
gw.mu.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,7 +359,7 @@ type urlWatcher struct {
|
||||||
// Batch registering saves CPU time needed for registering big number of Kubernetes objects
|
// Batch registering saves CPU time needed for registering big number of Kubernetes objects
|
||||||
// shared among big number of scrape jobs, since per-object labels are generated only once
|
// shared among big number of scrape jobs, since per-object labels are generated only once
|
||||||
// for all the scrape jobs (each scrape job is associated with a single apiWatcher).
|
// for all the scrape jobs (each scrape job is associated with a single apiWatcher).
|
||||||
// See reloadScrapeWorksForAPIWatchersLocked for details.
|
// See registerPendingAPIWatchersLocked for details.
|
||||||
awsPending map[*apiWatcher]struct{}
|
awsPending map[*apiWatcher]struct{}
|
||||||
|
|
||||||
// aws contains registered apiWatcher objects
|
// aws contains registered apiWatcher objects
|
||||||
|
@ -434,15 +415,45 @@ func (uw *urlWatcher) registerPendingAPIWatchersLocked() {
|
||||||
if len(uw.awsPending) == 0 {
|
if len(uw.awsPending) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
awsPending := make([]*apiWatcher, 0, len(uw.awsPending))
|
aws := make([]*apiWatcher, 0, len(uw.awsPending))
|
||||||
for aw := range uw.awsPending {
|
for aw := range uw.awsPending {
|
||||||
awsPending = append(awsPending, aw)
|
|
||||||
uw.aws[aw] = struct{}{}
|
uw.aws[aw] = struct{}{}
|
||||||
|
aws = append(aws, aw)
|
||||||
|
}
|
||||||
|
swosByKey := make([]map[string][]interface{}, len(aws))
|
||||||
|
for i := range aws {
|
||||||
|
swosByKey[i] = make(map[string][]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ScrapeWork objects in parallel on available CPU cores.
|
||||||
|
// This should reduce the time needed for their generation on systems with many CPU cores.
|
||||||
|
var swosByKeyLock sync.Mutex
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
limiterCh := make(chan struct{}, cgroup.AvailableCPUs())
|
||||||
|
for key, o := range uw.objectsByKey {
|
||||||
|
labels := o.getTargetLabels(uw.gw)
|
||||||
|
wg.Add(1)
|
||||||
|
limiterCh <- struct{}{}
|
||||||
|
go func(key string, labels []map[string]string) {
|
||||||
|
for i, aw := range aws {
|
||||||
|
swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels)
|
||||||
|
if len(swos) > 0 {
|
||||||
|
swosByKeyLock.Lock()
|
||||||
|
swosByKey[i][key] = swos
|
||||||
|
swosByKeyLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
<-limiterCh
|
||||||
|
}(key, labels)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
for i, aw := range aws {
|
||||||
|
aw.reloadScrapeWorks(uw, swosByKey[i])
|
||||||
}
|
}
|
||||||
uw.reloadScrapeWorksForAPIWatchersLocked(uw.awsPending)
|
|
||||||
uw.awsPending = make(map[*apiWatcher]struct{})
|
uw.awsPending = make(map[*apiWatcher]struct{})
|
||||||
metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="working"}`, uw.role)).Add(len(awsPending))
|
metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="working"}`, uw.role)).Add(len(aws))
|
||||||
metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="pending"}`, uw.role)).Add(-len(awsPending))
|
metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="pending"}`, uw.role)).Add(-len(aws))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uw *urlWatcher) unsubscribeAPIWatcherLocked(aw *apiWatcher) {
|
func (uw *urlWatcher) unsubscribeAPIWatcherLocked(aw *apiWatcher) {
|
||||||
|
@ -485,19 +496,21 @@ func (uw *urlWatcher) reloadObjects() string {
|
||||||
uw.gw.mu.Lock()
|
uw.gw.mu.Lock()
|
||||||
var updated, removed, added int
|
var updated, removed, added int
|
||||||
for key := range uw.objectsByKey {
|
for key := range uw.objectsByKey {
|
||||||
if _, ok := objectsByKey[key]; ok {
|
o, ok := objectsByKey[key]
|
||||||
|
if ok {
|
||||||
|
uw.updateObjectLocked(key, o)
|
||||||
updated++
|
updated++
|
||||||
} else {
|
} else {
|
||||||
|
uw.removeObjectLocked(key)
|
||||||
removed++
|
removed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for key := range objectsByKey {
|
for key, o := range objectsByKey {
|
||||||
if _, ok := uw.objectsByKey[key]; !ok {
|
if _, ok := uw.objectsByKey[key]; !ok {
|
||||||
|
uw.updateObjectLocked(key, o)
|
||||||
added++
|
added++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uw.objectsByKey = objectsByKey
|
|
||||||
uw.reloadScrapeWorksForAPIWatchersLocked(uw.aws)
|
|
||||||
uw.gw.mu.Unlock()
|
uw.gw.mu.Unlock()
|
||||||
|
|
||||||
uw.objectsUpdated.Add(updated)
|
uw.objectsUpdated.Add(updated)
|
||||||
|
@ -511,53 +524,6 @@ func (uw *urlWatcher) reloadObjects() string {
|
||||||
return uw.resourceVersion
|
return uw.resourceVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uw *urlWatcher) reloadScrapeWorksForAPIWatchersLocked(awsMap map[*apiWatcher]struct{}) {
|
|
||||||
if len(awsMap) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
aws := make([]*apiWatcher, 0, len(awsMap))
|
|
||||||
for aw := range awsMap {
|
|
||||||
aws = append(aws, aw)
|
|
||||||
}
|
|
||||||
swosByKey := make([]map[string][]interface{}, len(aws))
|
|
||||||
for i := range aws {
|
|
||||||
swosByKey[i] = make(map[string][]interface{})
|
|
||||||
}
|
|
||||||
// update swos concurrently,
|
|
||||||
// it must decrease reload time for high number of records at promscrape file
|
|
||||||
maxConcurrent := cgroup.AvailableCPUs() - 2
|
|
||||||
if maxConcurrent < 1 {
|
|
||||||
maxConcurrent = 1
|
|
||||||
}
|
|
||||||
limit := make(chan struct{}, maxConcurrent)
|
|
||||||
var (
|
|
||||||
mu sync.Mutex
|
|
||||||
wg sync.WaitGroup
|
|
||||||
)
|
|
||||||
for key, o := range uw.objectsByKey {
|
|
||||||
limit <- struct{}{}
|
|
||||||
wg.Add(1)
|
|
||||||
// update swsos for each target at separate CPU
|
|
||||||
go func(key string, o object) {
|
|
||||||
labels := o.getTargetLabels(uw.gw)
|
|
||||||
for i, aw := range aws {
|
|
||||||
swos := aw.getScrapeWorkObjectsForLabels(labels)
|
|
||||||
if len(swos) > 0 {
|
|
||||||
mu.Lock()
|
|
||||||
swosByKey[i][key] = swos
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
<-limit
|
|
||||||
}(key, o)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
for i, aw := range aws {
|
|
||||||
aw.reloadScrapeWorks(uw, swosByKey[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// watchForUpdates watches for object updates starting from uw.resourceVersion and updates the corresponding objects to the latest state.
|
// watchForUpdates watches for object updates starting from uw.resourceVersion and updates the corresponding objects to the latest state.
|
||||||
//
|
//
|
||||||
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes
|
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes
|
||||||
|
@ -636,20 +602,12 @@ func (uw *urlWatcher) readObjectUpdateStream(r io.Reader) error {
|
||||||
key := o.key()
|
key := o.key()
|
||||||
uw.gw.mu.Lock()
|
uw.gw.mu.Lock()
|
||||||
if _, ok := uw.objectsByKey[key]; !ok {
|
if _, ok := uw.objectsByKey[key]; !ok {
|
||||||
// if we.Type == "MODIFIED" is expected condition after recovering from the bookmarked resourceVersion.
|
|
||||||
uw.objectsCount.Inc()
|
uw.objectsCount.Inc()
|
||||||
uw.objectsAdded.Inc()
|
uw.objectsAdded.Inc()
|
||||||
} else {
|
} else {
|
||||||
// if we.Type == "ADDED" is expected condition after recovering from the bookmarked resourceVersion.
|
|
||||||
uw.objectsUpdated.Inc()
|
uw.objectsUpdated.Inc()
|
||||||
}
|
}
|
||||||
uw.objectsByKey[key] = o
|
uw.updateObjectLocked(key, o)
|
||||||
if len(uw.aws) > 0 {
|
|
||||||
labels := o.getTargetLabels(uw.gw)
|
|
||||||
for aw := range uw.aws {
|
|
||||||
aw.setScrapeWorks(uw, key, labels)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uw.gw.mu.Unlock()
|
uw.gw.mu.Unlock()
|
||||||
case "DELETED":
|
case "DELETED":
|
||||||
o, err := uw.parseObject(we.Object)
|
o, err := uw.parseObject(we.Object)
|
||||||
|
@ -661,11 +619,8 @@ func (uw *urlWatcher) readObjectUpdateStream(r io.Reader) error {
|
||||||
if _, ok := uw.objectsByKey[key]; ok {
|
if _, ok := uw.objectsByKey[key]; ok {
|
||||||
uw.objectsCount.Dec()
|
uw.objectsCount.Dec()
|
||||||
uw.objectsRemoved.Inc()
|
uw.objectsRemoved.Inc()
|
||||||
delete(uw.objectsByKey, key)
|
|
||||||
}
|
|
||||||
for aw := range uw.aws {
|
|
||||||
aw.removeScrapeWorks(uw, key)
|
|
||||||
}
|
}
|
||||||
|
uw.removeObjectLocked(key)
|
||||||
uw.gw.mu.Unlock()
|
uw.gw.mu.Unlock()
|
||||||
case "BOOKMARK":
|
case "BOOKMARK":
|
||||||
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#watch-bookmarks
|
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#watch-bookmarks
|
||||||
|
@ -692,6 +647,29 @@ func (uw *urlWatcher) readObjectUpdateStream(r io.Reader) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uw *urlWatcher) updateObjectLocked(key string, o object) {
|
||||||
|
oPrev, ok := uw.objectsByKey[key]
|
||||||
|
if ok && reflect.DeepEqual(oPrev, o) {
|
||||||
|
// Nothing to do, since the new object is equal to the previous one.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uw.objectsByKey[key] = o
|
||||||
|
if len(uw.aws) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
labels := o.getTargetLabels(uw.gw)
|
||||||
|
for aw := range uw.aws {
|
||||||
|
aw.setScrapeWorks(uw, key, labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uw *urlWatcher) removeObjectLocked(key string) {
|
||||||
|
delete(uw.objectsByKey, key)
|
||||||
|
for aw := range uw.aws {
|
||||||
|
aw.removeScrapeWorks(uw, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Bookmark is a bookmark message from Kubernetes Watch API.
|
// Bookmark is a bookmark message from Kubernetes Watch API.
|
||||||
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#watch-bookmarks
|
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#watch-bookmarks
|
||||||
type Bookmark struct {
|
type Bookmark struct {
|
||||||
|
|
Loading…
Reference in a new issue