/
resource.go
210 lines (180 loc) · 6.79 KB
/
resource.go
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
// Copyright 2016 Google, Inc.
//
// 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 doorman
import (
"path/filepath"
"sync"
"time"
log "github.com/golang/glog"
"github.com/golang/protobuf/proto"
pb "github.com/youtube/doorman/proto/doorman"
)
// NOTE(ryszard): Any exported Resource methods are responsible for
// taking the lock. Any NOT exported methods, on the other hand, must
// NOT take the lock, even to read. There can be multiple holders of a
// read lock, but any attempt to get the write lock will block further
// read locks. This means that getting a read lock recursively leads
// to deadlocks that are very hard to debug.
// Resource giving clients capacity leases to some resource. It is
// safe to call or access any exported methods or properties from
// multiple goroutines.
type Resource struct {
// ID is the name the clients use to access this resource.
ID string
// This mutex guards access to all properties defined below.
mu sync.RWMutex
// store contains leases granted to all clients for this
// resource.
store LeaseStore
algorithm Algorithm
learner Algorithm
learningModeEndTime time.Time
config *pb.ResourceTemplate
// expiryTime is the expiration time for this resource
// specified by a lower-level server (e.g. by the root server).
// The root server should ignore it, because it does not have
// any server which is lower it in the server tree.
expiryTime *time.Time
}
// capacity returns the current available capacity for res. Note: this
// does not lock the resource, and should be called only when the
// lock is already taken.
func (res *Resource) capacity() float64 {
if res.expiryTime != nil && res.expiryTime.Before(time.Now()) {
// FIXME(rushanny): probably here should be a safe capacity instead.
return 0.0
}
return res.config.GetCapacity()
}
// Release releases any resources held for client. This method is safe
// to call from multiple goroutines.
func (res *Resource) Release(client string) {
res.mu.Lock()
defer res.mu.Unlock()
res.store.Release(client)
}
// SetSafeCapacity sets the safe capacity in a response.
func (res *Resource) SetSafeCapacity(resp *pb.ResourceResponse) {
res.mu.RLock()
defer res.mu.RUnlock()
// If the resource configuration does not have a safe capacity
// configured we return a dynamic safe capacity which equals
// the capacity divided by the number of clients that we
// know about.
// TODO(josv): The calculation of the dynamic safe capacity
// needs to take sub clients into account (in a multi-server tree).
if res.config.SafeCapacity == nil {
resp.SafeCapacity = proto.Float64(*res.config.Capacity / float64(res.store.Count()))
} else {
resp.SafeCapacity = proto.Float64(*res.config.SafeCapacity)
}
}
// Decide runs an algorithm, and returns the leased assigned to
// client. learning should be true if the server is in learning mode.
func (res *Resource) Decide(request Request) Lease {
// NOTE(ryszard): Eventually the refresh interval should depend
// on the level of the server in the tree.
res.mu.Lock()
defer res.mu.Unlock()
res.store.Clean()
if res.learningModeEndTime.After(time.Now()) {
log.V(2).Infof("decision in learning mode for %v", res.ID)
return res.learner(res.store, res.capacity(), request)
}
return res.algorithm(res.store, res.capacity(), request)
}
// LoadConfig loads cfg into the resource. LoadConfig takes care of
// locking the resource.
func (res *Resource) LoadConfig(cfg *pb.ResourceTemplate, expiryTime *time.Time) {
res.mu.Lock()
defer res.mu.Unlock()
res.config = cfg
res.expiryTime = expiryTime
algo := cfg.GetAlgorithm()
res.algorithm = GetAlgorithm(algo)
res.learner = Learn(algo)
}
// Matches returns true if the resource ID matches the glob from cfg.
func (res *Resource) Matches(cfg *pb.ResourceTemplate) bool {
// NOTE(ryszard): The only possible error from Match is for a
// malformed pattern, so it is safe to quench it (especially
// that the config validation should have found any malformed
// patterns).
glob := cfg.GetIdentifierGlob()
matches, _ := filepath.Match(glob, res.ID)
return glob == res.ID || matches
}
// TODO(ryszard): Make it possible to use a different store.
// newResource returns a new resource named id and configured using
// cfg.
func (server *Server) newResource(id string, cfg *pb.ResourceTemplate) *Resource {
res := &Resource{
ID: id,
store: NewLeaseStore(id),
}
res.LoadConfig(cfg, nil)
// Calculates the learning mode end time. If one was not specified in the
// algorithm the learning mode duration equals the lease length, because
// that is the maximum time after which we can assume clients to have either
// reported in or lost their lease.
algo := res.config.GetAlgorithm()
var learningModeDuration time.Duration
if algo.LearningModeDuration != nil {
learningModeDuration = time.Duration(algo.GetLearningModeDuration()) * time.Second
} else {
learningModeDuration = time.Duration(algo.GetLeaseLength()) * time.Second
}
res.learningModeEndTime = server.GetLearningModeEndTime(learningModeDuration)
return res
}
// ResourceStatus is a view of a resource that is useful for
// reporting, eg in /statusz.
type ResourceStatus struct {
// ID is the id of the resource.
ID string
// SumHas is how much capacity has been given to clients.
SumHas float64
// SumWants is how much capacity the clients want.
SumWants float64
// Capacity is the total available capacity.
Capacity float64
// Count is the number of clients.
Count int64
// InLeaarningMode is true if the resource is in learning
// mode.
InLearningMode bool
// Algorithm is the algorithm with which this resource is
// configured.
Algorithm *pb.Algorithm
}
// Status returns a read-only view of res.
func (res *Resource) Status() ResourceStatus {
res.mu.RLock()
defer res.mu.RUnlock()
return ResourceStatus{
ID: res.ID,
SumHas: res.store.SumHas(),
SumWants: res.store.SumWants(),
Count: res.store.Count(),
Capacity: res.capacity(),
InLearningMode: res.learningModeEndTime.After(time.Now()),
Algorithm: res.config.Algorithm,
}
}
// ResourceLeaseStatus returns a read-only view of information the outstanding leases for this resource.
func (res *Resource) ResourceLeaseStatus() ResourceLeaseStatus {
res.mu.RLock()
defer res.mu.RUnlock()
return res.store.ResourceLeaseStatus()
}