forked from corestoreio/pkg
-
Notifications
You must be signed in to change notification settings - Fork 0
/
service_generic.go
364 lines (333 loc) · 13.8 KB
/
service_generic.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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
// Copyright 2015-2016, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// 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 cors
import (
"context"
"fmt"
"io"
"sort"
"sync"
"github.com/corestoreio/csfw/config"
"github.com/corestoreio/csfw/log"
"github.com/corestoreio/csfw/net/mw"
"github.com/corestoreio/csfw/store/scope"
"github.com/corestoreio/csfw/sync/singleflight"
"github.com/corestoreio/csfw/util/errors"
)
// Auto generated: Do not edit. See net/internal/scopedService package for more details.
type service struct {
// useWebsite internal flag used in configByContext(w,r) to tell the
// currenct handler if the scoped configuration is store or website based.
useWebsite bool
// optionAfterApply allows to set a custom function which runs every time
// after the options have been applied. Gets only executed if not nil.
optionAfterApply func() error
// rwmu protects all fields below
rwmu sync.RWMutex
// scopeCache internal cache for configurations.
scopeCache map[scope.TypeID]*ScopedConfig
// optionFactory optional configuration closure, can be nil. It pulls out
// the configuration settings from a slow backend during a request and
// caches the settings in the internal map. This function gets set via
// WithOptionFactory()
optionFactory OptionFactoryFunc
// optionInflight checks on a per scope.TypeID basis if the configuration
// loading process takes place. Stops the execution of other Goroutines (aka
// incoming requests) with the same scope.TypeID until the configuration has
// been fully loaded and applied for that specific scope. This function gets
// set via WithOptionFactory()
optionInflight *singleflight.Group
// ErrorHandler gets called whenever a programmer makes an error. Most two
// cases are: cannot extract scope from the context and scoped configuration
// is not valid. The default handler prints the error to the client and
// returns http.StatusServiceUnavailable
mw.ErrorHandler
// Log used for debugging. Defaults to black hole.
Log log.Logger
// rootConfig optional backend configuration. Gets only used while running
// HTTP related middlewares.
RootConfig config.Getter
}
func newService(opts ...Option) (*Service, error) {
s := &Service{
service: service{
Log: log.BlackHole{},
ErrorHandler: defaultErrorHandler,
scopeCache: make(map[scope.TypeID]*ScopedConfig),
},
}
if err := s.Options(WithDefaultConfig(scope.DefaultTypeID)); err != nil {
return nil, errors.Wrap(err, "[cors] Options WithDefaultConfig")
}
if err := s.Options(opts...); err != nil {
return nil, errors.Wrap(err, "[cors] Options any config")
}
return s, nil
}
// MustNew same as New() but panics on error. Use only during app start up process.
func MustNew(opts ...Option) *Service {
c, err := New(opts...)
if err != nil {
panic(err)
}
return c
}
// Options applies option at creation time or refreshes them.
func (s *Service) Options(opts ...Option) error {
for _, opt := range opts {
// opt can be nil because of the backend options where we have an array instead
// of a slice.
if opt != nil {
if err := opt(s); err != nil {
return errors.Wrap(err, "[cors] Service.Options")
}
}
}
if s.optionAfterApply != nil {
return errors.Wrap(s.optionAfterApply(), "[cors] optionValidation")
}
return nil
}
// ClearCache clears the internal map storing all scoped configurations. You
// must reapply all functional options.
// TODO(CyS) all previously applied options will be automatically reapplied.
func (s *Service) ClearCache() error {
s.scopeCache = make(map[scope.TypeID]*ScopedConfig)
return nil
}
// DebugCache uses Sprintf to write an ordered list (by scope.TypeID) into a
// writer. Only usable for debugging.
func (s *Service) DebugCache(w io.Writer) error {
s.rwmu.RLock()
defer s.rwmu.RUnlock()
srtScope := make(scope.TypeIDs, len(s.scopeCache))
var i int
for scp := range s.scopeCache {
srtScope[i] = scp
i++
}
sort.Sort(srtScope)
for _, scp := range srtScope {
scpCfg := s.scopeCache[scp]
if _, err := fmt.Fprintf(w, "%s => [%p]=%#v\n", scp, scpCfg, scpCfg); err != nil {
return errors.Wrap(err, "[cors] DebugCache Fprintf")
}
}
return nil
}
// ConfigByScope creates a new scoped configuration depending on the
// Service.useWebsite flag. If useWebsite==true the scoped configuration
// contains only the website->default scope despite setting a store scope. If an
// OptionFactory is set the configuration gets loaded from the backend. A nil
// root config causes a panic.
func (s *Service) ConfigByScope(websiteID, storeID int64) (ScopedConfig, error) {
cfg := s.RootConfig.NewScoped(websiteID, storeID)
if s.useWebsite {
cfg = s.RootConfig.NewScoped(websiteID, 0)
}
return s.ConfigByScopedGetter(cfg)
}
// configByContext extracts the scope (websiteID and storeID) from a context.
// The scoped configuration gets initialized by configFromScope() and returned.
// It panics if rootConfig if nil. Errors get not logged.
func (s *Service) configByContext(ctx context.Context) (ScopedConfig, error) {
// extract the scope out of the context and if not found a programmer made a
// mistake.
websiteID, storeID, scopeOK := scope.FromContext(ctx)
if !scopeOK {
return ScopedConfig{}, errors.NewNotFoundf("[cors] configByContext: scope.FromContext not found")
}
scpCfg, err := s.ConfigByScope(websiteID, storeID)
if err != nil {
// the scoped configuration is invalid and hence a programmer or package user
// made a mistake.
return ScopedConfig{}, errors.Wrap(err, "[cors] Service.configByContext.configFromScope") // rewrite error
}
return scpCfg, nil
}
// ConfigByScopedGetter returns the internal configuration depending on the
// ScopedGetter. Mainly used within the middleware. If you have applied the
// option WithOptionFactory() the configuration will be pulled out only one time
// from the backend configuration service. The field optionInflight handles the
// guaranteed atomic single loading for each scope.
func (s *Service) ConfigByScopedGetter(scpGet config.Scoped) (ScopedConfig, error) {
parent := scpGet.ParentID() // can be website or default
current := scpGet.ScopeID() // can be store or website or default
// 99.9999 % of the hits; 2nd argument must be zero because we must first
// test if a direct entry can be found; if not we must apply either the
// optionFactory function or do a fall back to the website scope and/or
// default scope.
if sCfg, err := s.ConfigByScopeID(current, 0); err == nil {
if s.Log.IsDebug() {
s.Log.Debug("cors.Service.ConfigByScopedGetter.IsValid",
log.Stringer("requested_scope", current),
log.Stringer("requested_parent_scope", scope.TypeID(0)),
log.Stringer("responded_scope", sCfg.ScopeID),
)
}
return sCfg, nil
}
// load the configuration from the slow backend. optionInflight guarantees
// that the closure will only be executed once but the returned result gets
// returned to all waiting goroutines.
if s.optionFactory != nil {
res, ok := <-s.optionInflight.DoChan(current.String(), func() (interface{}, error) {
if err := s.Options(s.optionFactory(scpGet)...); err != nil {
return ScopedConfig{}, errors.Wrap(err, "[cors] Options applied by OptionFactoryFunc")
}
sCfg, err := s.ConfigByScopeID(current, parent)
if s.Log.IsDebug() {
s.Log.Debug("cors.Service.ConfigByScopedGetter.Inflight.Do",
log.ErrWithKey("responded_scope_valid", err),
log.Stringer("requested_scope", current),
log.Stringer("requested_parent_scope", parent),
log.Stringer("responded_scope", sCfg.ScopeID),
log.Stringer("responded_parent", sCfg.ParentID),
)
}
return sCfg, errors.Wrap(err, "[cors] Options applied by OptionFactoryFunc")
})
if !ok { // unlikely to happen but you'll never know. how to test that?
return ScopedConfig{}, errors.NewFatalf("[cors] Inflight.DoChan returned a closed/unreadable channel")
}
if res.Err != nil {
return ScopedConfig{}, errors.Wrap(res.Err, "[cors] Inflight.DoChan.Error")
}
sCfg, ok := res.Val.(ScopedConfig)
if !ok {
return ScopedConfig{}, errors.NewFatalf("[cors] Inflight.DoChan res.Val cannot be type asserted to scopedConfig")
}
return sCfg, nil
}
sCfg, err := s.ConfigByScopeID(current, parent)
// under very high load: 20 users within 10 MicroSeconds this might get executed
// 1-3 times. more thinking needed.
if s.Log.IsDebug() {
s.Log.Debug("cors.Service.ConfigByScopedGetter.Parent",
log.Stringer("requested_scope", current),
log.Stringer("requested_parent_scope", parent),
log.Stringer("responded_scope", sCfg.ScopeID),
log.ErrWithKey("responded_scope_valid", err),
)
}
return sCfg, errors.Wrap(err, "[cors] Options applied and finaly validation")
}
// ConfigByScopeID returns the correct configuration for a scope and may fall
// back to the next higher scope: store -> website -> default. If `current`
// TypeID is Store, then the `parent` can only be Website or Default. If an
// entry for a scope cannot be found the next higher scope gets looked up and
// the pointer of the next higher scope gets assigned to the current scope. This
// prevents redundant configurations and enables us to change one scope
// configuration with an impact on all other scopes which depend on the parent
// scope. A zero `parent` triggers no further look ups. This function does not
// load any configuration (config.Getter related) from the backend and accesses
// the internal map of the Service directly.
//
// Important: a "current" scope cannot have multiple "parent" scopes.
func (s *Service) ConfigByScopeID(current scope.TypeID, parent scope.TypeID) (scpCfg ScopedConfig, _ error) {
// "current" can be Store or Website scope and "parent" can be Website or
// Default scope. If "parent" equals 0 then no fall back.
if !current.ValidParent(parent) {
return scpCfg, errors.NewNotValidf("[cors] The current scope %s has an invalid parent scope %s", current, parent)
}
// pointer must get dereferenced in a lock to avoid race conditions while
// reading in middleware the config values because we might execute the
// functional options for another scope while one scope runs in the
// middleware.
// lookup store/website scope. this should hit 99% of the calls of this function.
s.rwmu.RLock()
pScpCfg, ok := s.scopeCache[current]
if ok && pScpCfg != nil {
scpCfg = *pScpCfg
}
s.rwmu.RUnlock()
if ok {
return scpCfg, errors.Wrap(scpCfg.isValid(), "[cors] Validated directly found")
}
if parent == 0 {
return scpCfg, errors.NewNotFoundf(errConfigNotFound, current)
}
// slow path: now lock everything until the fall back has been found.
s.rwmu.Lock()
defer s.rwmu.Unlock()
// if the current scope cannot be found, fall back to parent scope and apply
// the maybe found configuration to the current scope configuration.
if !ok && parent.Type() == scope.Website {
pScpCfg, ok = s.scopeCache[parent]
if ok && pScpCfg != nil {
pScpCfg.ParentID = parent
scpCfg = *pScpCfg
if err := scpCfg.isValid(); err != nil {
return ScopedConfig{}, errors.Wrap(err, "[cors] Error in Website scope configuration")
}
s.scopeCache[current] = pScpCfg // gets assigned a pointer so equal to parent
return scpCfg, nil
}
}
// if the current and parent scope cannot be found, fall back to default
// scope and apply the maybe found configuration to the current scope
// configuration.
if !ok {
pScpCfg, ok = s.scopeCache[scope.DefaultTypeID]
if ok && pScpCfg != nil {
pScpCfg.ParentID = scope.DefaultTypeID
scpCfg = *pScpCfg
if err := scpCfg.isValid(); err != nil {
return ScopedConfig{}, errors.Wrap(err, "[cors] error in default configuration")
}
s.scopeCache[current] = pScpCfg // gets assigned a pointer so equal to default
} else {
return scpCfg, errors.NewNotFoundf(errConfigNotFound, scope.DefaultTypeID)
}
}
return scpCfg, nil
}
// findScopedConfig used in functional options to look up if a parent
// configuration exists and if not creates a newScopedConfig(). The
// scope.DefaultTypeID will always be appended to the end of the provided
// arguments. This function acquires a lock. You must call its buddy function
// updateScopedConfig() to close the lock.
func (s *Service) findScopedConfig(scopeIDs ...scope.TypeID) *ScopedConfig {
s.rwmu.Lock() // Unlock() in updateScopedConfig()
target, parents := scope.TypeIDs(scopeIDs).TargetAndParents()
sc := s.scopeCache[target]
if sc != nil {
return sc
}
// "parents" contains now the next higher scopes, at least minimum the
// DefaultTypeID. For example if we have as "target" scope Store then
// "parents" would contain Website and/or Default, depending on how many
// arguments have been applied in a functional option.
for _, id := range parents {
if sc, ok := s.scopeCache[id]; ok && sc != nil {
shallowCopy := new(ScopedConfig)
*shallowCopy = *sc
shallowCopy.ParentID = id
shallowCopy.ScopeID = target
return shallowCopy
}
}
// if parents[0] panics for being out of bounds then something is really wrong.
return newScopedConfig(target, parents[0])
}
// updateScopedConfig used in functional options to store a scoped configuration
// in the internal cache. This function gets called in a function option at the
// end after applying the new configuration value. This function releases an
// already acquired lock. You can call its buddy function findScopedConfig() to
// acquire a lock.
func (s *Service) updateScopedConfig(sc *ScopedConfig) error {
s.scopeCache[sc.ScopeID] = sc
s.rwmu.Unlock()
return nil
}