From de741dea6df4c785f68331f5d3fc2a531a304874 Mon Sep 17 00:00:00 2001 From: func25 Date: Sat, 4 Jul 2026 22:46:38 +0700 Subject: [PATCH 1/6] fix --- docs/victorialogs/CHANGELOG.md | 1 + lib/logstorage/storage_search.go | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/docs/victorialogs/CHANGELOG.md b/docs/victorialogs/CHANGELOG.md index 5cfffe1d15..c458ccf14f 100644 --- a/docs/victorialogs/CHANGELOG.md +++ b/docs/victorialogs/CHANGELOG.md @@ -38,6 +38,7 @@ according to the following docs: * BUGFIX: [cluster version](https://docs.victoriametrics.com/victorialogs/cluster/): avoid `cannot connect to storage node at ...: EOF` errors after `vlselect` or `vlinsert` was idle for more than 60 seconds. See [#1440](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1440). * BUGFIX: [vlselect](https://docs.victoriametrics.com/victorialogs/cluster/): return `502 Bad Gateway` HTTP response code for incoming queries when one of the `vlstorage` nodes runs a VictoriaLogs version with an incompatible internal API instead of `400 Bad Request`. This is consistent with the `502 Bad Gateway` response returned when a `vlstorage` node is unavailable, and it allows building a proper failover scheme in high-availability setups. See [these docs](https://docs.victoriametrics.com/victorialogs/cluster/#high-availability). * BUGFIX: [multi-level cluster setup](https://docs.victoriametrics.com/victorialogs/cluster/#multi-level-cluster-setup): properly return `502 Bad Gateway` HTTP response code when a `vlselect` node queries other `vlselect` nodes and the underlying `vlstorage` is unavailable, as described at [high availability](https://docs.victoriametrics.com/victorialogs/cluster/#high-availability) docs. This allows configuring proper failover schemes to a healthy cluster. +* BUGFIX: [`/select/tenant_ids`](https://docs.victoriametrics.com/victorialogs/querying/#querying-tenants): return tenant ids in a deterministic (sorted) order. Previously the order was random across requests. ## [v1.51.0](https://github.com/VictoriaMetrics/VictoriaLogs/releases/tag/v1.51.0) diff --git a/lib/logstorage/storage_search.go b/lib/logstorage/storage_search.go index 7ead24a81c..30ca54ec6c 100644 --- a/lib/logstorage/storage_search.go +++ b/lib/logstorage/storage_search.go @@ -236,6 +236,11 @@ func (s *Storage) runQuery(qctx *QueryContext, writeBlock writeBlockResultFunc) } func (s *Storage) getSearchOptions(tenantIDs []TenantID, q *Query, hiddenFieldsFilters []string) *storageSearchOptions { + // tenantIDs must be sorted, since block search performs binary search over them. + sort.Slice(tenantIDs, func(i, j int) bool { + return tenantIDs[i].less(&tenantIDs[j]) + }) + streamIDs := q.getStreamIDs() sort.Slice(streamIDs, func(i, j int) bool { return streamIDs[i].less(&streamIDs[j]) @@ -752,6 +757,11 @@ func (s *Storage) getTenantIDs(ctx context.Context, start, end int64) ([]TenantI for k := range uniqTenantIDs { tenants = append(tenants, k) } + // Sort tenants, since they are collected from a map with random iteration order. + // This provides stable output and a sorted list for callers that rely on it. + sort.Slice(tenants, func(i, j int) bool { + return tenants[i].less(&tenants[j]) + }) return tenants, nil } From dc23fefb88d0e22bef4c99cc8ad8f792253f91e9 Mon Sep 17 00:00:00 2001 From: func25 Date: Sat, 4 Jul 2026 22:50:04 +0700 Subject: [PATCH 2/6] update changelog --- docs/victorialogs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/victorialogs/CHANGELOG.md b/docs/victorialogs/CHANGELOG.md index c458ccf14f..b564890c0b 100644 --- a/docs/victorialogs/CHANGELOG.md +++ b/docs/victorialogs/CHANGELOG.md @@ -38,7 +38,7 @@ according to the following docs: * BUGFIX: [cluster version](https://docs.victoriametrics.com/victorialogs/cluster/): avoid `cannot connect to storage node at ...: EOF` errors after `vlselect` or `vlinsert` was idle for more than 60 seconds. See [#1440](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1440). * BUGFIX: [vlselect](https://docs.victoriametrics.com/victorialogs/cluster/): return `502 Bad Gateway` HTTP response code for incoming queries when one of the `vlstorage` nodes runs a VictoriaLogs version with an incompatible internal API instead of `400 Bad Request`. This is consistent with the `502 Bad Gateway` response returned when a `vlstorage` node is unavailable, and it allows building a proper failover scheme in high-availability setups. See [these docs](https://docs.victoriametrics.com/victorialogs/cluster/#high-availability). * BUGFIX: [multi-level cluster setup](https://docs.victoriametrics.com/victorialogs/cluster/#multi-level-cluster-setup): properly return `502 Bad Gateway` HTTP response code when a `vlselect` node queries other `vlselect` nodes and the underlying `vlstorage` is unavailable, as described at [high availability](https://docs.victoriametrics.com/victorialogs/cluster/#high-availability) docs. This allows configuring proper failover schemes to a healthy cluster. -* BUGFIX: [`/select/tenant_ids`](https://docs.victoriametrics.com/victorialogs/querying/#querying-tenants): return tenant ids in a deterministic (sorted) order. Previously the order was random across requests. +* BUGFIX: [`/select/tenant_ids`](https://docs.victoriametrics.com/victorialogs/querying/#querying-tenants): return tenant ids in a deterministic (sorted) order. Previously the order was random across requests. See [#1575](https://github.com/VictoriaMetrics/VictoriaLogs/pull/1575). ## [v1.51.0](https://github.com/VictoriaMetrics/VictoriaLogs/releases/tag/v1.51.0) From 0760c2d8df074621db3ed6fa8ee3efd0bfb223fe Mon Sep 17 00:00:00 2001 From: func25 Date: Sun, 5 Jul 2026 11:26:57 +0700 Subject: [PATCH 3/6] update --- app/vlselect/logsql/logsql.go | 2 ++ app/vlstorage/netselect/netselect.go | 1 + lib/logstorage/storage_search.go | 8 ++------ lib/logstorage/storage_search_test.go | 8 ++------ lib/logstorage/tenant_id.go | 8 ++++++++ 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/vlselect/logsql/logsql.go b/app/vlselect/logsql/logsql.go index 2599ca2da9..5a1eca7ccf 100644 --- a/app/vlselect/logsql/logsql.go +++ b/app/vlselect/logsql/logsql.go @@ -1394,6 +1394,8 @@ func ProcessTenantIDsRequest(ctx context.Context, w http.ResponseWriter, r *http httpserver.Errorf(w, r, "cannot obtain tenantIDs: %s", err) return } + // Return tenants in a deterministic (sorted) order for stable API responses. + logstorage.SortTenantIDs(tenants) data, err := json.Marshal(tenants) if err != nil { diff --git a/app/vlstorage/netselect/netselect.go b/app/vlstorage/netselect/netselect.go index 55ae0551e8..44bda0e518 100644 --- a/app/vlstorage/netselect/netselect.go +++ b/app/vlstorage/netselect/netselect.go @@ -679,6 +679,7 @@ func (s *Storage) getTenantIDs(ctx context.Context, start, end int64) ([]logstor tenantIDs = append(tenantIDs, tenantID) } + logstorage.SortTenantIDs(tenantIDs) return tenantIDs, nil } diff --git a/lib/logstorage/storage_search.go b/lib/logstorage/storage_search.go index 30ca54ec6c..dcf0072d94 100644 --- a/lib/logstorage/storage_search.go +++ b/lib/logstorage/storage_search.go @@ -237,9 +237,7 @@ func (s *Storage) runQuery(qctx *QueryContext, writeBlock writeBlockResultFunc) func (s *Storage) getSearchOptions(tenantIDs []TenantID, q *Query, hiddenFieldsFilters []string) *storageSearchOptions { // tenantIDs must be sorted, since block search performs binary search over them. - sort.Slice(tenantIDs, func(i, j int) bool { - return tenantIDs[i].less(&tenantIDs[j]) - }) + SortTenantIDs(tenantIDs) streamIDs := q.getStreamIDs() sort.Slice(streamIDs, func(i, j int) bool { @@ -759,9 +757,7 @@ func (s *Storage) getTenantIDs(ctx context.Context, start, end int64) ([]TenantI } // Sort tenants, since they are collected from a map with random iteration order. // This provides stable output and a sorted list for callers that rely on it. - sort.Slice(tenants, func(i, j int) bool { - return tenants[i].less(&tenants[j]) - }) + SortTenantIDs(tenants) return tenants, nil } diff --git a/lib/logstorage/storage_search_test.go b/lib/logstorage/storage_search_test.go index 9a56f39982..9591865888 100644 --- a/lib/logstorage/storage_search_test.go +++ b/lib/logstorage/storage_search_test.go @@ -675,12 +675,8 @@ func TestStorageRunQuery(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s", err) } - sort.Slice(tenantIDs, func(i, j int) bool { - return tenantIDs[i].less(&tenantIDs[j]) - }) - sort.Slice(allTenantIDs, func(i, j int) bool { - return allTenantIDs[i].less(&allTenantIDs[j]) - }) + SortTenantIDs(tenantIDs) + SortTenantIDs(allTenantIDs) if !reflect.DeepEqual(tenantIDs, allTenantIDs) { t.Fatalf("unexpected GetTenantIDs result; got: %v, want: %v", tenantIDs, allTenantIDs) } diff --git a/lib/logstorage/tenant_id.go b/lib/logstorage/tenant_id.go index d8706197cf..8608231fd4 100644 --- a/lib/logstorage/tenant_id.go +++ b/lib/logstorage/tenant_id.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "sort" "strconv" "strings" @@ -46,6 +47,13 @@ func (tid *TenantID) less(a *TenantID) bool { return tid.ProjectID < a.ProjectID } +// SortTenantIDs sorts tenantIDs in place in ascending order. +func SortTenantIDs(tenantIDs []TenantID) { + sort.Slice(tenantIDs, func(i, j int) bool { + return tenantIDs[i].less(&tenantIDs[j]) + }) +} + func (tid *TenantID) marshalString(dst []byte) []byte { n := uint64(tid.AccountID)<<32 | uint64(tid.ProjectID) dst = marshalUint64Hex(dst, n) From 1f6c48716eb77dc3d109437c26d778498e42755c Mon Sep 17 00:00:00 2001 From: func25 Date: Sun, 5 Jul 2026 11:33:55 +0700 Subject: [PATCH 4/6] remove redundant sort --- app/vlselect/logsql/logsql.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/vlselect/logsql/logsql.go b/app/vlselect/logsql/logsql.go index 5a1eca7ccf..2599ca2da9 100644 --- a/app/vlselect/logsql/logsql.go +++ b/app/vlselect/logsql/logsql.go @@ -1394,8 +1394,6 @@ func ProcessTenantIDsRequest(ctx context.Context, w http.ResponseWriter, r *http httpserver.Errorf(w, r, "cannot obtain tenantIDs: %s", err) return } - // Return tenants in a deterministic (sorted) order for stable API responses. - logstorage.SortTenantIDs(tenants) data, err := json.Marshal(tenants) if err != nil { From 4d95c1faae7a1d62191dbaaf4ed1617e1d200e17 Mon Sep 17 00:00:00 2001 From: func25 Date: Sun, 5 Jul 2026 11:41:58 +0700 Subject: [PATCH 5/6] improve comments a bit --- app/vlstorage/main.go | 2 +- app/vlstorage/netselect/netselect.go | 2 +- lib/logstorage/storage_search.go | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/vlstorage/main.go b/app/vlstorage/main.go index bce1b14577..e9a7806694 100644 --- a/app/vlstorage/main.go +++ b/app/vlstorage/main.go @@ -665,7 +665,7 @@ func DeleteActiveTasks(ctx context.Context) ([]*logstorage.DeleteTask, error) { return netstorageSelect.DeleteActiveTasks(ctx) } -// GetTenantIDs returns tenantIDs from the storage by the given start and end. +// GetTenantIDs returns sorted tenantIDs from the storage by the given start and end. func GetTenantIDs(ctx context.Context, start, end int64) ([]logstorage.TenantID, error) { if localStorage != nil { return localStorage.GetTenantIDs(ctx, start, end) diff --git a/app/vlstorage/netselect/netselect.go b/app/vlstorage/netselect/netselect.go index 44bda0e518..b237a8b82e 100644 --- a/app/vlstorage/netselect/netselect.go +++ b/app/vlstorage/netselect/netselect.go @@ -630,7 +630,7 @@ func (s *Storage) DeleteActiveTasks(ctx context.Context) ([]*logstorage.DeleteTa return tasks, nil } -// GetTenantIDs returns tenantIDs for the given start and end. +// GetTenantIDs returns sorted tenantIDs for the given start and end. func (s *Storage) GetTenantIDs(ctx context.Context, start, end int64) ([]logstorage.TenantID, error) { return s.getTenantIDs(ctx, start, end) } diff --git a/lib/logstorage/storage_search.go b/lib/logstorage/storage_search.go index dcf0072d94..e936f8de51 100644 --- a/lib/logstorage/storage_search.go +++ b/lib/logstorage/storage_search.go @@ -681,7 +681,7 @@ func (s *Storage) GetStreamIDs(qctx *QueryContext, limit uint64) ([]ValueWithHit return s.GetFieldValues(qctx, "_stream_id", "", limit) } -// GetTenantIDs returns tenantIDs for the given start and end. +// GetTenantIDs returns sorted tenantIDs for the given start and end. func (s *Storage) GetTenantIDs(ctx context.Context, start, end int64) ([]TenantID, error) { return s.getTenantIDs(ctx, start, end) } @@ -755,10 +755,8 @@ func (s *Storage) getTenantIDs(ctx context.Context, start, end int64) ([]TenantI for k := range uniqTenantIDs { tenants = append(tenants, k) } - // Sort tenants, since they are collected from a map with random iteration order. - // This provides stable output and a sorted list for callers that rely on it. - SortTenantIDs(tenants) + SortTenantIDs(tenants) return tenants, nil } From 26dcaea20a1279aa4902d7c8df31a69909f88db1 Mon Sep 17 00:00:00 2001 From: func25 Date: Sun, 5 Jul 2026 11:44:32 +0700 Subject: [PATCH 6/6] no need sorting in test --- lib/logstorage/storage_search_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logstorage/storage_search_test.go b/lib/logstorage/storage_search_test.go index 9591865888..723fa773c2 100644 --- a/lib/logstorage/storage_search_test.go +++ b/lib/logstorage/storage_search_test.go @@ -675,7 +675,7 @@ func TestStorageRunQuery(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s", err) } - SortTenantIDs(tenantIDs) + // GetTenantIDs must return tenantIDs in sorted order. SortTenantIDs(allTenantIDs) if !reflect.DeepEqual(tenantIDs, allTenantIDs) { t.Fatalf("unexpected GetTenantIDs result; got: %v, want: %v", tenantIDs, allTenantIDs)