From c32f39efeea0e8de984d1722eeb8e2ebaaf84415 Mon Sep 17 00:00:00 2001 From: Alex Weaver Date: Fri, 13 Dec 2024 11:10:30 -0600 Subject: [PATCH 1/5] Show tokens in partition ring status page --- ring/partition_ring_http.go | 10 ++++++++++ ring/partition_ring_status.gohtml | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/ring/partition_ring_http.go b/ring/partition_ring_http.go index 8fe3778eb..c37d91c58 100644 --- a/ring/partition_ring_http.go +++ b/ring/partition_ring_http.go @@ -68,6 +68,8 @@ func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req * State: partition.State, StateTimestamp: partition.GetStateTime(), OwnerIDs: owners, + Tokens: partition.Tokens, + NumTokens: len(partition.Tokens), } } @@ -83,6 +85,8 @@ func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req * State: PartitionUnknown, StateTimestamp: time.Time{}, OwnerIDs: []string{ownerID}, + Tokens: partition.Tokens, + NumTokens: len(partition.Tokens), } partitionsByID[owner.OwnedPartition] = partition @@ -105,6 +109,8 @@ func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req * return partitions[i].ID < partitions[j].ID }) + tokensParam := req.URL.Query().Get("tokens") + renderHTTPResponse(w, partitionRingPageData{ Partitions: partitions, PartitionStateChanges: map[PartitionState]PartitionState{ @@ -112,6 +118,7 @@ func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req * PartitionActive: PartitionInactive, PartitionInactive: PartitionActive, }, + ShowTokens: tokensParam == "true", }, partitionRingPageTemplate, req) } @@ -146,6 +153,7 @@ type partitionRingPageData struct { // PartitionStateChanges maps the allowed state changes through the UI. PartitionStateChanges map[PartitionState]PartitionState `json:"-"` + ShowTokens bool `json:"-"` } type partitionPageData struct { @@ -154,4 +162,6 @@ type partitionPageData struct { State PartitionState `json:"state"` StateTimestamp time.Time `json:"state_timestamp"` OwnerIDs []string `json:"owner_ids"` + Tokens []uint32 `json:"tokens"` + NumTokens int `json:"-"` } diff --git a/ring/partition_ring_status.gohtml b/ring/partition_ring_status.gohtml index f4f9afe87..427a294e9 100644 --- a/ring/partition_ring_status.gohtml +++ b/ring/partition_ring_status.gohtml @@ -15,6 +15,8 @@ State State updated at Owners + Tokens + Ownership Actions @@ -42,6 +44,7 @@ {{$ownerID}}
{{ end }} + {{ .NumTokens }} {{ if and (not .Corrupted) (ne (index $stateChanges .State) 0) }} @@ -59,5 +62,23 @@ {{ end }} +
+ {{ if .ShowTokens }} + + {{ else }} + + {{ end }} + + {{ if .ShowTokens }} + {{ range $i, $partition := .Partitions }} +

Instance: {{ .ID }}

+

+ Tokens:
+ {{ range $token := .Tokens }} + {{ $token }} + {{ end }} +

+ {{ end }} + {{ end }} \ No newline at end of file From 2b766ee2442a00aad26e7779a79be200a8829801 Mon Sep 17 00:00:00 2001 From: Alex Weaver Date: Thu, 2 Jan 2025 16:09:27 -0600 Subject: [PATCH 2/5] Show ownership column --- ring/partition_ring_http.go | 13 ++++ ring/partition_ring_http_test.go | 98 +++++++++++++++++++++++-------- ring/partition_ring_model.go | 28 +++++++++ ring/partition_ring_status.gohtml | 3 +- 4 files changed, 116 insertions(+), 26 deletions(-) diff --git a/ring/partition_ring_http.go b/ring/partition_ring_http.go index c37d91c58..698f33b0f 100644 --- a/ring/partition_ring_http.go +++ b/ring/partition_ring_http.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "html/template" + "math" "net/http" "slices" "sort" @@ -18,6 +19,9 @@ var partitionRingPageTemplate = template.Must(template.New("webpage").Funcs(temp "mod": func(i, j int32) bool { return i%j == 0 }, + "humanFloat": func(f float64) string { + return fmt.Sprintf("%.3g", f) + }, "formatTimestamp": func(ts time.Time) string { return ts.Format("2006-01-02 15:04:05 MST") }, @@ -55,6 +59,7 @@ func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req * ring = h.reader.PartitionRing() ringDesc = ring.desc ) + ownedTokens := ringDesc.countTokens() // Prepare the data to render partitions in the page. partitionsByID := make(map[int32]partitionPageData, len(ringDesc.Partitions)) @@ -70,6 +75,7 @@ func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req * OwnerIDs: owners, Tokens: partition.Tokens, NumTokens: len(partition.Tokens), + Ownership: distancePercentage(ownedTokens[id]), } } @@ -87,6 +93,7 @@ func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req * OwnerIDs: []string{ownerID}, Tokens: partition.Tokens, NumTokens: len(partition.Tokens), + Ownership: distancePercentage(ownedTokens[owner.OwnedPartition]), } partitionsByID[owner.OwnedPartition] = partition @@ -164,4 +171,10 @@ type partitionPageData struct { OwnerIDs []string `json:"owner_ids"` Tokens []uint32 `json:"tokens"` NumTokens int `json:"-"` + Ownership float64 `json:"-"` +} + +// distancePercentage renders a given token distance as the percentage of all possible token values covered by that distance. +func distancePercentage(distance int64) float64 { + return (float64(distance) / float64(math.MaxUint32)) * 100 } diff --git a/ring/partition_ring_http_test.go b/ring/partition_ring_http_test.go index 2b73321ea..e52e8183e 100644 --- a/ring/partition_ring_http_test.go +++ b/ring/partition_ring_http_test.go @@ -28,10 +28,12 @@ func TestPartitionRingPageHandler_ViewPage(t *testing.T) { 1: { State: PartitionActive, StateTimestamp: time.Now().Unix(), + Tokens: []uint32{1000000, 3000000, 6000000}, }, 2: { State: PartitionInactive, StateTimestamp: time.Now().Unix(), + Tokens: []uint32{2000000, 4000000, 5000000, 7000000}, }, }, Owners: map[string]OwnerDesc{ @@ -59,31 +61,77 @@ func TestPartitionRingPageHandler_ViewPage(t *testing.T) { ) recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/partition-ring", nil)) - - assert.Equal(t, http.StatusOK, recorder.Code) - assert.Equal(t, "text/html", recorder.Header().Get("Content-Type")) - - assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ - "", "1", "", - "", "Active", "", - "", "[^<]+", "", - "", "ingester-zone-a-0", "
", "ingester-zone-b-0", "
", "", - }, `\s*`))), recorder.Body.String()) - - assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ - "", "2", "", - "", "Inactive", "", - "", "[^<]+", "", - "", "ingester-zone-a-1", "
", "ingester-zone-b-1", "
", "", - }, `\s*`))), recorder.Body.String()) - - assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ - "", "3", "", - "", "Corrupt", "", - "", "N/A", "", - "", "ingester-zone-b-2", "
", "", - }, `\s*`))), recorder.Body.String()) + + t.Run("displays expected partition info", func(t *testing.T) { + handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/partition-ring", nil)) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, "text/html", recorder.Header().Get("Content-Type")) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + "", "1", "", + "", "Active", "", + "", "[^<]+", "", + "", "ingester-zone-a-0", "
", "ingester-zone-b-0", "
", "", + "", "3", "", + "", "99.9%", "", + }, `\s*`))), recorder.Body.String()) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + "", "2", "", + "", "Inactive", "", + "", "[^<]+", "", + "", "ingester-zone-a-1", "
", "ingester-zone-b-1", "
", "", + "", "4", "", + "", "0.0931%", "", + }, `\s*`))), recorder.Body.String()) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + "", "3", "", + "", "Corrupt", "", + "", "N/A", "", + "", "ingester-zone-b-2", "
", "", + "", "0", "", + "", "0%", "", + }, `\s*`))), recorder.Body.String()) + }) + + t.Run("displays Show Tokens button by default", func(t *testing.T) { + handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/partition-ring", nil)) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, "text/html", recorder.Header().Get("Content-Type")) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + ``, + }, `\s*`))), recorder.Body.String()) + }) + + t.Run("displays tokens when Show Tokens is enabled", func(t *testing.T) { + handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/partition-ring?tokens=true", nil)) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, "text/html", recorder.Header().Get("Content-Type")) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + ``, + }, `\s*`))), recorder.Body.String()) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + "

", "Instance: 1", "

", + "

", "Tokens:
", "1000000", "3000000", "6000000", "

", + }, `\s*`))), recorder.Body.String()) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + "

", "Instance: 2", "

", + "

", "Tokens:
", "2000000", "4000000", "5000000", "7000000", "

", + }, `\s*`))), recorder.Body.String()) + + assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ + "

", "Instance: 3", "

", + "

", "Tokens:
", "

", + }, `\s*`))), recorder.Body.String()) + }) } func TestPartitionRingPageHandler_ChangePartitionState(t *testing.T) { diff --git a/ring/partition_ring_model.go b/ring/partition_ring_model.go index f957fe6b8..cecda6b89 100644 --- a/ring/partition_ring_model.go +++ b/ring/partition_ring_model.go @@ -94,6 +94,34 @@ func (m *PartitionRingDesc) partitionByToken() map[Token]int32 { return out } +// CountTokens returns the summed token distance of all tokens in each partition. +func (m *PartitionRingDesc) countTokens() map[int32]int64 { + owned := make(map[int32]int64, len(m.Partitions)) + sortedTokens := m.tokens() + tokensToPartitions := m.partitionByToken() + + for i, token := range sortedTokens { + partition := tokensToPartitions[Token(token)] + + var prevToken uint32 + if i == 0 { + prevToken = sortedTokens[len(sortedTokens)-1] + } else { + prevToken = sortedTokens[i-1] + } + diff := tokenDistance(prevToken, token) + owned[partition] = owned[partition] + diff + } + + // Partitions with 0 tokens should still exist in the result. + for id := range m.Partitions { + if _, ok := owned[id]; !ok { + owned[id] = 0 + } + } + return owned +} + // ownersByPartition returns a map where the key is the partition ID and the value is a list of owner IDs. func (m *PartitionRingDesc) ownersByPartition() map[int32][]string { out := make(map[int32][]string, len(m.Partitions)) diff --git a/ring/partition_ring_status.gohtml b/ring/partition_ring_status.gohtml index 427a294e9..d63efd6ee 100644 --- a/ring/partition_ring_status.gohtml +++ b/ring/partition_ring_status.gohtml @@ -45,6 +45,7 @@ {{ end }} {{ .NumTokens }} + {{ .Ownership | humanFloat }}% {{ if and (not .Corrupted) (ne (index $stateChanges .State) 0) }} @@ -64,7 +65,7 @@
{{ if .ShowTokens }} - + {{ else }} {{ end }} From f785e5e62b75a6a883d9559a7f08f660641389a6 Mon Sep 17 00:00:00 2001 From: Alex Weaver Date: Thu, 2 Jan 2025 16:23:21 -0600 Subject: [PATCH 3/5] Additional countTokens tests --- ring/partition_ring_model_test.go | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/ring/partition_ring_model_test.go b/ring/partition_ring_model_test.go index 183b9bf56..ca71429b4 100644 --- a/ring/partition_ring_model_test.go +++ b/ring/partition_ring_model_test.go @@ -2,6 +2,7 @@ package ring import ( "fmt" + "math" "testing" "time" @@ -79,6 +80,51 @@ func TestPartitionRingDesc_countPartitionsByState(t *testing.T) { }) } +func TestPartitionRingDesc_countTokens(t *testing.T) { + t.Run("empty ring should return an empty result", func(t *testing.T) { + desc := &PartitionRingDesc{} + + result := desc.countTokens() + + assert.Empty(t, result) + }) + + t.Run("ring with some partitions should return correct distances", func(t *testing.T) { + desc := &PartitionRingDesc{ + Partitions: map[int32]PartitionDesc{ + 1: {Tokens: []uint32{1000000, 3000000, 6000000}}, + 2: {Tokens: []uint32{2000000, 4000000, 8000000}}, + 3: {Tokens: []uint32{5000000, 9000000}}, + }, + } + + result := desc.countTokens() + + expected := map[int32]int64{ + 1: 3000000 + (int64(math.MaxUint32) + 1 - 9000000), + 2: 4000000, + 3: 2000000, + } + assert.Equal(t, expected, result) + }) + + t.Run("partitions with no tokens should be present in the result, with 0 distance", func(t *testing.T) { + desc := &PartitionRingDesc{ + Partitions: map[int32]PartitionDesc{ + 1: {Tokens: []uint32{1000000, 3000000, 6000000}}, + 2: {Tokens: []uint32{2000000, 4000000, 8000000}}, + 3: {Tokens: []uint32{5000000, 9000000}}, + 4: {Tokens: []uint32{}}, + }, + } + + result := desc.countTokens() + + assert.Contains(t, result, int32(4)) + assert.Equal(t, int64(0), result[4]) + }) +} + func TestPartitionRingDesc_AddOrUpdateOwner(t *testing.T) { now := time.Now() From 76389b86e3c0f33774a00e540caf048768ddc384 Mon Sep 17 00:00:00 2001 From: Alex Weaver Date: Thu, 2 Jan 2025 17:14:58 -0600 Subject: [PATCH 4/5] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd0c1657..ee1781868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ * [FEATURE] Add methods `Increment`, `FlushAll`, `CompareAndSwap`, `Touch` to `cache.MemcachedClient` #477 * [FEATURE] Add `concurrency.ForEachJobMergeResults()` utility function. #486 * [FEATURE] Add `ring.DoMultiUntilQuorumWithoutSuccessfulContextCancellation()`. #495 +* [ENHANCEMENT] Display token information in partition ring status page #631 * [ENHANCEMENT] Add ability to log all source hosts from http header instead of only the first one. #444 * [ENHANCEMENT] Add configuration to customize backoff for the gRPC clients. * [ENHANCEMENT] Use `SecretReader` interface to fetch secrets when configuring TLS. #274 From b560d702b5ad982895a3732cb5d0bb385c333f50 Mon Sep 17 00:00:00 2001 From: Alex Weaver Date: Fri, 3 Jan 2025 09:56:57 -0600 Subject: [PATCH 5/5] Rename header from Instance to Partition in show tokens mode --- ring/partition_ring_http_test.go | 6 +++--- ring/partition_ring_status.gohtml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ring/partition_ring_http_test.go b/ring/partition_ring_http_test.go index e52e8183e..aef11603d 100644 --- a/ring/partition_ring_http_test.go +++ b/ring/partition_ring_http_test.go @@ -118,17 +118,17 @@ func TestPartitionRingPageHandler_ViewPage(t *testing.T) { }, `\s*`))), recorder.Body.String()) assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ - "

", "Instance: 1", "

", + "

", "Partition: 1", "

", "

", "Tokens:
", "1000000", "3000000", "6000000", "

", }, `\s*`))), recorder.Body.String()) assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ - "

", "Instance: 2", "

", + "

", "Partition: 2", "

", "

", "Tokens:
", "2000000", "4000000", "5000000", "7000000", "

", }, `\s*`))), recorder.Body.String()) assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{ - "

", "Instance: 3", "

", + "

", "Partition: 3", "

", "

", "Tokens:
", "

", }, `\s*`))), recorder.Body.String()) }) diff --git a/ring/partition_ring_status.gohtml b/ring/partition_ring_status.gohtml index d63efd6ee..1f0a2eaf0 100644 --- a/ring/partition_ring_status.gohtml +++ b/ring/partition_ring_status.gohtml @@ -72,7 +72,7 @@ {{ if .ShowTokens }} {{ range $i, $partition := .Partitions }} -

Instance: {{ .ID }}

+

Partition: {{ .ID }}

Tokens:
{{ range $token := .Tokens }}