summaryrefslogtreecommitdiffhomepage
path: root/usagepool.go
blob: 7007849fb958b61ca582336db7c7ed384e7c4337 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// Copyright 2015 Matthew Holt and The Caddy 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.

package caddy

import (
	"fmt"
	"sync"
	"sync/atomic"
)

// UsagePool is a thread-safe map that pools values
// based on usage (reference counting). Values are
// only inserted if they do not already exist. There
// are two ways to add values to the pool:
//
//  1. LoadOrStore will increment usage and store the
//     value immediately if it does not already exist.
//  2. LoadOrNew will atomically check for existence
//     and construct the value immediately if it does
//     not already exist, or increment the usage
//     otherwise, then store that value in the pool.
//     When the constructed value is finally deleted
//     from the pool (when its usage reaches 0), it
//     will be cleaned up by calling Destruct().
//
// The use of LoadOrNew allows values to be created
// and reused and finally cleaned up only once, even
// though they may have many references throughout
// their lifespan. This is helpful, for example, when
// sharing thread-safe io.Writers that you only want
// to open and close once.
//
// There is no way to overwrite existing keys in the
// pool without first deleting it as many times as it
// was stored. Deleting too many times will panic.
//
// The implementation does not use a sync.Pool because
// UsagePool needs additional atomicity to run the
// constructor functions when creating a new value when
// LoadOrNew is used. (We could probably use sync.Pool
// but we'd still have to layer our own additional locks
// on top.)
//
// An empty UsagePool is NOT safe to use; always call
// NewUsagePool() to make a new one.
type UsagePool struct {
	sync.RWMutex
	pool map[any]*usagePoolVal
}

// NewUsagePool returns a new usage pool that is ready to use.
func NewUsagePool() *UsagePool {
	return &UsagePool{
		pool: make(map[any]*usagePoolVal),
	}
}

// LoadOrNew loads the value associated with key from the pool if it
// already exists. If the key doesn't exist, it will call construct
// to create a new value and then stores that in the pool. An error
// is only returned if the constructor returns an error. The loaded
// or constructed value is returned. The loaded return value is true
// if the value already existed and was loaded, or false if it was
// newly constructed.
func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loaded bool, err error) {
	var upv *usagePoolVal
	up.Lock()
	upv, loaded = up.pool[key]
	if loaded {
		atomic.AddInt32(&upv.refs, 1)
		up.Unlock()
		upv.RLock()
		value = upv.value
		err = upv.err
		upv.RUnlock()
	} else {
		upv = &usagePoolVal{refs: 1}
		upv.Lock()
		up.pool[key] = upv
		up.Unlock()
		value, err = construct()
		if err == nil {
			upv.value = value
		} else {
			upv.err = err
			up.Lock()
			// this *should* be safe, I think, because we have a
			// write lock on upv, but we might also need to ensure
			// that upv.err is nil before doing this, since we
			// released the write lock on up during construct...
			// but then again it's also after midnight...
			delete(up.pool, key)
			up.Unlock()
		}
		upv.Unlock()
	}
	return
}

// LoadOrStore loads the value associated with key from the pool if it
// already exists, or stores it if it does not exist. It returns the
// value that was either loaded or stored, and true if the value already
// existed and was
func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
	var upv *usagePoolVal
	up.Lock()
	upv, loaded = up.pool[key]
	if loaded {
		atomic.AddInt32(&upv.refs, 1)
		up.Unlock()
		upv.Lock()
		if upv.err == nil {
			value = upv.value
		} else {
			upv.value = val
			upv.err = nil
		}
		upv.Unlock()
	} else {
		upv = &usagePoolVal{refs: 1, value: val}
		up.pool[key] = upv
		up.Unlock()
		value = val
	}
	return
}

// Range iterates the pool similarly to how sync.Map.Range() does:
// it calls f for every key in the pool, and if f returns false,
// iteration is stopped. Ranging does not affect usage counts.
//
// This method is somewhat naive and acquires a read lock on the
// entire pool during iteration, so do your best to make f() really
// fast, m'kay?
func (up *UsagePool) Range(f func(key, value any) bool) {
	up.RLock()
	defer up.RUnlock()
	for key, upv := range up.pool {
		upv.RLock()
		if upv.err != nil {
			upv.RUnlock()
			continue
		}
		val := upv.value
		upv.RUnlock()
		if !f(key, val) {
			break
		}
	}
}

// Delete decrements the usage count for key and removes the
// value from the underlying map if the usage is 0. It returns
// true if the usage count reached 0 and the value was deleted.
// It panics if the usage count drops below 0; always call
// Delete precisely as many times as LoadOrStore.
func (up *UsagePool) Delete(key any) (deleted bool, err error) {
	up.Lock()
	upv, ok := up.pool[key]
	if !ok {
		up.Unlock()
		return false, nil
	}
	refs := atomic.AddInt32(&upv.refs, -1)
	if refs == 0 {
		delete(up.pool, key)
		up.Unlock()
		upv.RLock()
		val := upv.value
		upv.RUnlock()
		if destructor, ok := val.(Destructor); ok {
			err = destructor.Destruct()
		}
		deleted = true
	} else {
		up.Unlock()
		if refs < 0 {
			panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
				upv.value, upv.refs))
		}
	}
	return
}

// References returns the number of references (count of usages) to a
// key in the pool, and true if the key exists, or false otherwise.
func (up *UsagePool) References(key any) (int, bool) {
	up.RLock()
	upv, loaded := up.pool[key]
	up.RUnlock()
	if loaded {
		// I wonder if it'd be safer to read this value during
		// our lock on the UsagePool... guess we'll see...
		refs := atomic.LoadInt32(&upv.refs)
		return int(refs), true
	}
	return 0, false
}

// Constructor is a function that returns a new value
// that can destruct itself when it is no longer needed.
type Constructor func() (Destructor, error)

// Destructor is a value that can clean itself up when
// it is deallocated.
type Destructor interface {
	Destruct() error
}

type usagePoolVal struct {
	refs  int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
	value any
	err   error
	sync.RWMutex
}