github.com/mailgun/holster/v4@v4.20.0/collections/ttlmap.go (about)

     1  /*
     2  Copyright 2017 Mailgun Technologies Inc
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  package collections
    17  
    18  import (
    19  	"fmt"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/mailgun/holster/v4/clock"
    24  )
    25  
    26  type TTLMap struct {
    27  	// Optionally specifies a callback function to be
    28  	// executed when an entry has expired
    29  	OnExpire func(key string, i interface{})
    30  
    31  	capacity    int
    32  	elements    map[string]*mapElement
    33  	expiryTimes *PriorityQueue
    34  	mutex       *sync.RWMutex
    35  }
    36  
    37  type mapElement struct {
    38  	key    string
    39  	value  interface{}
    40  	heapEl *PQItem
    41  }
    42  
    43  func NewTTLMap(capacity int) *TTLMap {
    44  	if capacity <= 0 {
    45  		capacity = 0
    46  	}
    47  
    48  	return &TTLMap{
    49  		capacity:    capacity,
    50  		elements:    make(map[string]*mapElement),
    51  		expiryTimes: NewPriorityQueue(),
    52  		mutex:       &sync.RWMutex{},
    53  	}
    54  }
    55  
    56  func (m *TTLMap) Set(key string, value interface{}, ttlSeconds int) error {
    57  	expiryTime, err := m.toEpochSeconds(ttlSeconds)
    58  	if err != nil {
    59  		return err
    60  	}
    61  	m.mutex.Lock()
    62  	defer m.mutex.Unlock()
    63  	m.set(key, value, expiryTime)
    64  	return nil
    65  }
    66  
    67  func (m *TTLMap) Len() int {
    68  	m.mutex.RLock()
    69  	defer m.mutex.RUnlock()
    70  	return len(m.elements)
    71  }
    72  
    73  func (m *TTLMap) Get(key string) (interface{}, bool) {
    74  	value, mapEl, expired := m.lockNGet(key)
    75  	if mapEl == nil {
    76  		return nil, false
    77  	}
    78  	if expired {
    79  		m.lockNDel(mapEl)
    80  		return nil, false
    81  	}
    82  	return value, true
    83  }
    84  
    85  func (m *TTLMap) Increment(key string, value, ttlSeconds int) (retval int, reterr error) {
    86  	expiryTime, err := m.toEpochSeconds(ttlSeconds)
    87  	if err != nil {
    88  		return 0, err
    89  	}
    90  
    91  	m.mutex.Lock()
    92  	defer m.mutex.Unlock()
    93  
    94  	mapEl, expired := m.get(key)
    95  	if mapEl == nil || expired {
    96  		m.set(key, value, expiryTime)
    97  		return value, nil
    98  	}
    99  
   100  	currentValue, ok := mapEl.value.(int)
   101  	if !ok {
   102  		return 0, fmt.Errorf("Expected existing value to be integer, got %T", mapEl.value) //nolint:stylecheck // TODO(v5): ST1005: error strings should not be capitalized
   103  	}
   104  
   105  	currentValue += value
   106  	m.set(key, currentValue, expiryTime)
   107  	return currentValue, nil
   108  }
   109  
   110  func (m *TTLMap) GetInt(key string) (retval int, found bool, reterr error) {
   111  	valueI, exists := m.Get(key)
   112  	if !exists {
   113  		return 0, false, nil
   114  	}
   115  	value, ok := valueI.(int)
   116  	if !ok {
   117  		return 0, false, fmt.Errorf("Expected existing value to be integer, got %T", valueI) //nolint:stylecheck // TODO(v5): ST1005: error strings should not be capitalized
   118  	}
   119  	return value, true, nil
   120  }
   121  
   122  func (m *TTLMap) set(key string, value interface{}, expiryTime int) {
   123  	if mapEl, ok := m.elements[key]; ok {
   124  		mapEl.value = value
   125  		m.expiryTimes.Update(mapEl.heapEl, expiryTime)
   126  		return
   127  	}
   128  
   129  	if len(m.elements) >= m.capacity {
   130  		m.freeSpace(1)
   131  	}
   132  	heapEl := &PQItem{
   133  		Priority: expiryTime,
   134  	}
   135  	mapEl := &mapElement{
   136  		key:    key,
   137  		value:  value,
   138  		heapEl: heapEl,
   139  	}
   140  	heapEl.Value = mapEl
   141  	m.elements[key] = mapEl
   142  	m.expiryTimes.Push(heapEl)
   143  }
   144  
   145  func (m *TTLMap) lockNGet(key string) (value interface{}, mapEl *mapElement, expired bool) {
   146  	m.mutex.RLock()
   147  	defer m.mutex.RUnlock()
   148  
   149  	mapEl, expired = m.get(key)
   150  	value = nil
   151  	if mapEl != nil {
   152  		value = mapEl.value
   153  	}
   154  	return value, mapEl, expired
   155  }
   156  
   157  func (m *TTLMap) get(key string) (*mapElement, bool) {
   158  	mapEl, ok := m.elements[key]
   159  	if !ok {
   160  		return nil, false
   161  	}
   162  	now := int(clock.Now().Unix())
   163  	expired := mapEl.heapEl.Priority <= now
   164  	return mapEl, expired
   165  }
   166  
   167  func (m *TTLMap) lockNDel(mapEl *mapElement) {
   168  	m.mutex.Lock()
   169  	defer m.mutex.Unlock()
   170  
   171  	// Map element could have been updated. Now that we have a lock
   172  	// retrieve it again and check if it is still expired.
   173  	var ok bool
   174  	if mapEl, ok = m.elements[mapEl.key]; !ok {
   175  		return
   176  	}
   177  	now := int(clock.Now().Unix())
   178  	if mapEl.heapEl.Priority > now {
   179  		return
   180  	}
   181  
   182  	if m.OnExpire != nil {
   183  		m.OnExpire(mapEl.key, mapEl.value)
   184  	}
   185  
   186  	delete(m.elements, mapEl.key)
   187  	m.expiryTimes.Remove(mapEl.heapEl)
   188  }
   189  
   190  func (m *TTLMap) freeSpace(count int) {
   191  	removed := m.RemoveExpired(count)
   192  	if removed >= count {
   193  		return
   194  	}
   195  	m.RemoveLastUsed(count - removed)
   196  }
   197  
   198  func (m *TTLMap) RemoveExpired(iterations int) int {
   199  	removed := 0
   200  	now := int(clock.Now().Unix())
   201  	for i := 0; i < iterations; i += 1 {
   202  		if len(m.elements) == 0 {
   203  			break
   204  		}
   205  		heapEl := m.expiryTimes.Peek()
   206  		if heapEl.Priority > now {
   207  			break
   208  		}
   209  		m.expiryTimes.Pop()
   210  		mapEl := heapEl.Value.(*mapElement)
   211  		delete(m.elements, mapEl.key)
   212  		removed += 1
   213  	}
   214  	return removed
   215  }
   216  
   217  func (m *TTLMap) RemoveLastUsed(iterations int) {
   218  	for i := 0; i < iterations; i += 1 {
   219  		if len(m.elements) == 0 {
   220  			return
   221  		}
   222  		heapEl := m.expiryTimes.Pop()
   223  		mapEl := heapEl.Value.(*mapElement)
   224  		delete(m.elements, mapEl.key)
   225  	}
   226  }
   227  
   228  func (m *TTLMap) toEpochSeconds(ttlSeconds int) (int, error) {
   229  	if ttlSeconds <= 0 {
   230  		return 0, fmt.Errorf("ttlSeconds should be >= 0, got %d", ttlSeconds)
   231  	}
   232  	return int(clock.Now().Add(time.Second * time.Duration(ttlSeconds)).Unix()), nil
   233  }