Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge feat-shared-interner to release-2.30.2-grafana #23

Merged
merged 2 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions pkg/intern/intern.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2019 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Inspired / copied / modified from https://gitlab.com/cznic/strutil/blob/master/strutil.go,
// which is MIT licensed, so:
//
// Copyright (c) 2014 The strutil Authors. All rights reserved.

package intern

import (
"sync"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/prometheus/pkg/labels"
"go.uber.org/atomic"
)

// Shared interner
var (
Global Interner = New(prometheus.DefaultRegisterer)
)

// Interner is a string interner.
type Interner interface {
// Metrics returns Metrics for the interner.
Metrics() *Metrics

// Intern will intern an input string, returning the interned string as
// a result.
Intern(string) string

// Release removes an interned string from interner.
Release(string)
}

func New(r prometheus.Registerer) Interner {
return &pool{
m: NewMetrics(r),
pool: map[string]*entry{},
}
}

type Metrics struct {
Strings prometheus.Gauge
NoReferenceReleases prometheus.Counter
}

func NewMetrics(r prometheus.Registerer) *Metrics {
var m Metrics
m.Strings = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prometheus",
Subsystem: "interner",
Name: "num_strings",
Help: "The current number of interned strings",
})
m.NoReferenceReleases = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "prometheus",
Subsystem: "interner",
Name: "string_interner_zero_reference_releases_total",
Help: "The number of times release has been called for strings that are not interned.",
})

if r != nil {
r.MustRegister(m.Strings)
r.MustRegister(m.NoReferenceReleases)
}

return &m
}

type pool struct {
m *Metrics

mtx sync.RWMutex
pool map[string]*entry
}

type entry struct {
refs atomic.Int64

s string
}

func newEntry(s string) *entry {
return &entry{s: s}
}

func (p *pool) Metrics() *Metrics { return p.m }

func (p *pool) Intern(s string) string {
if s == "" {
return ""
}

p.mtx.RLock()
interned, ok := p.pool[s]
p.mtx.RUnlock()
if ok {
interned.refs.Inc()
return interned.s
}
p.mtx.Lock()
defer p.mtx.Unlock()
if interned, ok := p.pool[s]; ok {
interned.refs.Inc()
return interned.s
}

p.m.Strings.Inc()
p.pool[s] = newEntry(s)
p.pool[s].refs.Store(1)
return s
}

func (p *pool) Release(s string) {
p.mtx.RLock()
interned, ok := p.pool[s]
p.mtx.RUnlock()

if !ok {
p.m.NoReferenceReleases.Inc()
return
}

refs := interned.refs.Dec()
if refs > 0 {
return
}

p.mtx.Lock()
defer p.mtx.Unlock()
if interned.refs.Load() != 0 {
return
}
p.m.Strings.Dec()
delete(p.pool, s)
}

// Intern is a helper function for interning all label names and values to a
// given interner.
func Intern(interner Interner, lbls labels.Labels) {
for i, l := range lbls {
lbls[i].Name = interner.Intern(l.Name)
lbls[i].Value = interner.Intern(l.Value)
}
}

// Release is a helper function for releasing all label names and values from a
// given interner.
func Release(interner Interner, ls labels.Labels) {
for _, l := range ls {
interner.Release(l.Name)
interner.Release(l.Value)
}
}
26 changes: 13 additions & 13 deletions storage/remote/intern_test.go → pkg/intern/intern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
//
// Copyright (c) 2014 The strutil Authors. All rights reserved.

package remote
package intern

import (
"fmt"
Expand All @@ -27,59 +27,59 @@ import (
)

func TestIntern(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern"
interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))
}

func TestIntern_MultiRef(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern_MultiRef"

interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))

interner.intern(testString)
interner.Intern(testString)
interned, ok = interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(2), interned.refs.Load(), fmt.Sprintf("expected refs to be 2 but it was %d", interned.refs.Load()))
}

func TestIntern_DeleteRef(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern_DeleteRef"

interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))

interner.release(testString)
interner.Release(testString)
_, ok = interner.pool[testString]
require.Equal(t, false, ok)
}

func TestIntern_MultiRef_Concurrent(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern_MultiRef_Concurrent"

interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]
require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))

go interner.release(testString)
go interner.Release(testString)

interner.intern(testString)
interner.Intern(testString)

time.Sleep(time.Millisecond)

Expand Down
Loading