diff --git a/changelog/28062.txt b/changelog/28062.txt new file mode 100644 index 000000000000..c6a1fbef1980 --- /dev/null +++ b/changelog/28062.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core/activity: Ensure client count queries that include the current month return consistent results by sorting the clients before performing estimation +``` \ No newline at end of file diff --git a/vault/activity_log_util_common.go b/vault/activity_log_util_common.go index dd4314d01366..322bfa6914b1 100644 --- a/vault/activity_log_util_common.go +++ b/vault/activity_log_util_common.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "slices" "sort" "strings" "time" @@ -156,20 +157,30 @@ func (a *ActivityLog) computeCurrentMonthForBillingPeriodInternal(ctx context.Co return nil, errors.New("malformed current month used to calculate current month's activity") } - for nsID, namespace := range month.Namespaces { + namespaces := month.Namespaces.sort() + for _, n := range namespaces { + nsID := n.id + namespace := n.processByNamespace namespaceActivity := &activity.MonthlyNamespaceRecord{NamespaceID: nsID, Counts: &activity.CountsRecord{}} newNamespaceActivity := &activity.MonthlyNamespaceRecord{NamespaceID: nsID, Counts: &activity.CountsRecord{}} mountsActivity := make([]*activity.MountRecord, 0) newMountsActivity := make([]*activity.MountRecord, 0) - for mountAccessor, mount := range namespace.Mounts { + mounts := namespace.Mounts.sort() + for _, m := range mounts { + mountAccessor := m.accessor + mount := m.processMount mountPath := a.mountAccessorToMountPath(mountAccessor) mountCounts := &activity.CountsRecord{} newMountCounts := &activity.CountsRecord{} for _, typ := range ActivityClientTypes { - for clientID := range mount.Counts.clientsByType(typ) { + clients := mount.Counts.clientsByType(typ) + clientIDs := clients.sort() + + // sort the client IDs before inserting + for _, clientID := range clientIDs { hllByType[typ].Insert([]byte(clientID)) // increment the per mount, per namespace, and total counts @@ -241,6 +252,47 @@ func (a *ActivityLog) incrementCount(c *activity.CountsRecord, num int, typ stri } } +type processByNamespaceID struct { + id string + *processByNamespace +} + +func (s summaryByNamespace) sort() []*processByNamespaceID { + namespaces := make([]*processByNamespaceID, 0, len(s)) + for nsID, namespace := range s { + namespaces = append(namespaces, &processByNamespaceID{id: nsID, processByNamespace: namespace}) + } + slices.SortStableFunc(namespaces, func(a, b *processByNamespaceID) int { + return strings.Compare(a.id, b.id) + }) + return namespaces +} + +type processMountAccessor struct { + accessor string + *processMount +} + +func (s summaryByMount) sort() []*processMountAccessor { + mounts := make([]*processMountAccessor, 0, len(s)) + for mountAccessor, mount := range s { + mounts = append(mounts, &processMountAccessor{accessor: mountAccessor, processMount: mount}) + } + slices.SortStableFunc(mounts, func(a, b *processMountAccessor) int { + return strings.Compare(a.accessor, b.accessor) + }) + return mounts +} + +func (c clientIDSet) sort() []string { + clientIDs := make([]string, 0, len(c)) + for clientID := range c { + clientIDs = append(clientIDs, clientID) + } + sort.Strings(clientIDs) + return clientIDs +} + // sortALResponseNamespaces sorts the namespaces for activity log responses. func (a *ActivityLog) sortALResponseNamespaces(byNamespaceResponse []*ResponseNamespace) { sort.Slice(byNamespaceResponse, func(i, j int) bool {